From 86559e332a546cd85bc171fe92cb3597a2be4aa3 Mon Sep 17 00:00:00 2001 From: Till <253026766+philtk79@users.noreply.github.com> Date: Wed, 27 May 2026 15:50:33 +0200 Subject: [PATCH] feat: add gateway tokenreview rbac initializer Add a TokenReviewRBAC subroutine that grants the GraphQL gateway scoped identity permission to perform TokenReview in org and account workspaces. The subroutine resolves the gateway home logical cluster ID and binds system:cluster: to both tokenreviews:create and system:kcp:workspace:access in target workspaces, including account sub-workspaces and parent root:orgs. Signed-off-by: Till <253026766+philtk79@users.noreply.github.com> --- internal/config/config.go | 3 + .../accountlogicalcluster_controller.go | 12 +- .../orglogicalcluster_controller.go | 5 + internal/subroutine/tokenreview_rbac.go | 221 +++++++++++++++++ internal/subroutine/tokenreview_rbac_test.go | 228 ++++++++++++++++++ 5 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 internal/subroutine/tokenreview_rbac.go create mode 100644 internal/subroutine/tokenreview_rbac_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 2f28730d..1b4d5a9c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -32,6 +32,7 @@ type InitializerConfig struct { IDPEnabled bool InviteEnabled bool WorkspaceAuthEnabled bool + TokenReviewRBACEnabled bool } type FGAConfig struct { @@ -129,6 +130,7 @@ func NewConfig() Config { IDPEnabled: true, InviteEnabled: true, WorkspaceAuthEnabled: true, + TokenReviewRBACEnabled: true, }, Webhooks: WebhooksConfig{ Port: 9443, @@ -176,6 +178,7 @@ func (c *Config) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&c.Initializer.IDPEnabled, "initializer-idp-enabled", c.Initializer.IDPEnabled, "Enable IDP initialization") fs.BoolVar(&c.Initializer.InviteEnabled, "initializer-invite-enabled", c.Initializer.InviteEnabled, "Enable invite initialization") fs.BoolVar(&c.Initializer.WorkspaceAuthEnabled, "initializer-workspace-auth-enabled", c.Initializer.WorkspaceAuthEnabled, "Enable workspace auth initialization") + fs.BoolVar(&c.Initializer.TokenReviewRBACEnabled, "initializer-tokenreview-rbac-enabled", c.Initializer.TokenReviewRBACEnabled, "Enable gateway TokenReview RBAC bindings in org and account workspaces") fs.StringSliceVar(&c.AdditionalAudiences, "additional-audiences", c.AdditionalAudiences, "Additional audiences to trust in workspace JWT authentication configurations") fs.BoolVar(&c.Webhooks.Enabled, "webhooks-enabled", c.Webhooks.Enabled, "Enable validating webhooks") fs.IntVar(&c.Webhooks.Port, "webhooks-port", c.Webhooks.Port, "Set webhook server port") diff --git a/internal/controller/accountlogicalcluster_controller.go b/internal/controller/accountlogicalcluster_controller.go index f4b5db89..264094cd 100644 --- a/internal/controller/accountlogicalcluster_controller.go +++ b/internal/controller/accountlogicalcluster_controller.go @@ -15,6 +15,7 @@ import ( "github.com/platform-mesh/security-operator/internal/fga" "github.com/platform-mesh/security-operator/internal/metrics" "github.com/platform-mesh/security-operator/internal/subroutine" + "github.com/platform-mesh/subroutines" "github.com/platform-mesh/subroutines/lifecycle" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -42,9 +43,18 @@ func NewAccountLogicalClusterController(log *logger.Logger, cfg config.Config, f return nil, fmt.Errorf("creating RateLimiter: %w", err) } + var subs []subroutines.Subroutine + subs = append(subs, subroutine.NewAccountTuplesSubroutine(mgr, fgaClient, storeIDGetter, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType, kcpClientGetter)) + // Account workspaces (root:orgs::, nested accounts, etc.) are + // where the portal often queries GraphQL. The gateway must TokenReview in + // those paths, so the same RBAC bindings as org workspaces are required here. + if cfg.Initializer.TokenReviewRBACEnabled { + subs = append(subs, subroutine.NewTokenReviewRBACSubroutine(kcpClientGetter)) + } + lc := lifecycle.New(mgr, opts.Name, func() client.Object { return &kcpcorev1alpha1.LogicalCluster{} - }, subroutine.NewAccountTuplesSubroutine(mgr, fgaClient, storeIDGetter, cfg.FGA.CreatorRelation, cfg.FGA.ParentRelation, cfg.FGA.ObjectType, kcpClientGetter)) + }, subs...) if opts.InitializerName != "" { lc = lc.WithInitializer(opts.InitializerName) diff --git a/internal/controller/orglogicalcluster_controller.go b/internal/controller/orglogicalcluster_controller.go index 652b691d..ca58a04f 100644 --- a/internal/controller/orglogicalcluster_controller.go +++ b/internal/controller/orglogicalcluster_controller.go @@ -70,6 +70,11 @@ func NewOrgLogicalClusterController(log *logger.Logger, kcpClientGetter iclient. if cfg.Initializer.WorkspaceAuthEnabled { subs = append(subs, subroutine.NewWorkspaceAuthConfigurationSubroutine(inClusterClient, mgr, kcpClientGetter, cfg)) } + // Org workspaces (root:orgs:) and the shared parent root:orgs need + // gateway TokenReview RBAC; see internal/subroutine/tokenreview_rbac.go. + if cfg.Initializer.TokenReviewRBACEnabled { + subs = append(subs, subroutine.NewTokenReviewRBACSubroutine(kcpClientGetter)) + } lc := lifecycle.New(mgr, opts.Name, func() client.Object { return &kcpcorev1alpha1.LogicalCluster{} diff --git a/internal/subroutine/tokenreview_rbac.go b/internal/subroutine/tokenreview_rbac.go new file mode 100644 index 00000000..1a7f2cea --- /dev/null +++ b/internal/subroutine/tokenreview_rbac.go @@ -0,0 +1,221 @@ +// TokenReview RBAC support for kubernetes-graphql-gateway cross-workspace auth. +// +// kubernetes-graphql-gateway validates end-user JWTs by calling the Kubernetes +// TokenReview API in the *target* workspace (e.g. root:orgs:org1:account1). The +// gateway runs with a provider-scoped kubeconfig from root:platform-mesh-system; +// kcp rewrites that identity cross-workspace to +// Group system:cluster:. That group needs +// tokenreviews:create and system:kcp:workspace:access in every workspace the +// portal queries—not only org roots (root:orgs:) but also account +// sub-workspaces (root:orgs::, including nested accounts). +package subroutine + +import ( + "context" + "fmt" + + iclient "github.com/platform-mesh/security-operator/internal/client" + "github.com/platform-mesh/security-operator/internal/config" + "github.com/platform-mesh/subroutines" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" +) + +const ( + // Fixed object names written into each target workspace. + gatewayTokenReviewClusterRoleName = "platform-mesh:gateway-tokenreview" + gatewayTokenReviewClusterRoleBindingName = "platform-mesh:gateway-tokenreview" + gatewayTokenReviewWorkspaceAccessBindingName = "platform-mesh:gateway-tokenreview-workspace-access" + gatewayTokenReviewWorkspaceAccessClusterRole = "system:kcp:workspace:access" + + // Workspace where the gateway's provider kubeconfig is scoped (its "home"). + gatewayHomeWorkspacePath = "root:platform-mesh-system" +) + +type tokenReviewRBACSubroutine struct { + kcpClientGetter iclient.KCPClientGetter + + // cachedGatewayHomeClusterID is set after the first successful lookup only. + // Transient lookup failures are not cached so controller-runtime backoff can retry. + cachedGatewayHomeClusterID string +} + +// NewTokenReviewRBACSubroutine ensures ClusterRole(Binding)s so the graphql +// gateway can perform TokenReview in org and account workspaces. +func NewTokenReviewRBACSubroutine(kcpClientGetter iclient.KCPClientGetter) *tokenReviewRBACSubroutine { + return &tokenReviewRBACSubroutine{ + kcpClientGetter: kcpClientGetter, + } +} + +var ( + _ subroutines.Initializer = &tokenReviewRBACSubroutine{} + _ subroutines.Processor = &tokenReviewRBACSubroutine{} +) + +func (r *tokenReviewRBACSubroutine) GetName() string { return "TokenReviewRBAC" } + +func (r *tokenReviewRBACSubroutine) Initialize(ctx context.Context, obj client.Object) (subroutines.Result, error) { + return r.reconcile(ctx, obj) +} + +func (r *tokenReviewRBACSubroutine) Process(ctx context.Context, obj client.Object) (subroutines.Result, error) { + return r.reconcile(ctx, obj) +} + +func (r *tokenReviewRBACSubroutine) reconcile(ctx context.Context, obj client.Object) (subroutines.Result, error) { + lc := obj.(*kcpcorev1alpha1.LogicalCluster) + + workspacePath := lc.Annotations["kcp.io/path"] + if workspacePath == "" { + return subroutines.OK(), fmt.Errorf("LogicalCluster %s has no kcp.io/path annotation", lc.Name) + } + + gatewayHomeClusterID, err := r.gatewayHomeClusterID(ctx) + if err != nil { + return subroutines.OK(), fmt.Errorf("failed to resolve gateway home cluster ID: %w", err) + } + + if err := r.ensureCrossWorkspaceTokenReviewRBAC(ctx, workspacePath, lc, gatewayHomeClusterID); err != nil { + return subroutines.OK(), err + } + + // The portal welcome page queries GraphQL at root:orgs (parent of all orgs). + // Bindings on individual org/account workspaces alone are not enough for that path. + if err := r.ensureParentOrgsWorkspaceBindings(ctx, workspacePath, gatewayHomeClusterID); err != nil { + return subroutines.OK(), err + } + + return subroutines.OK(), nil +} + +func (r *tokenReviewRBACSubroutine) ensureParentOrgsWorkspaceBindings(ctx context.Context, workspacePath, gatewayHomeClusterID string) error { + if workspacePath == config.OrgsClusterPath { + return nil + } + if err := r.ensureCrossWorkspaceTokenReviewRBAC(ctx, config.OrgsClusterPath, nil, gatewayHomeClusterID); err != nil { + return fmt.Errorf("failed to ensure gateway TokenReview RBAC in %s: %w", config.OrgsClusterPath, err) + } + return nil +} + +// ensureCrossWorkspaceTokenReviewRBAC writes the ClusterRole and two ClusterRoleBindings +// that grant the gateway's cross-workspace identity permission to create TokenReviews +// and access the workspace in workspacePath. +func (r *tokenReviewRBACSubroutine) ensureCrossWorkspaceTokenReviewRBAC( + ctx context.Context, + workspacePath string, + owner client.Object, + gatewayHomeClusterID string, +) error { + cl, err := r.kcpClientGetter.NewClientForLogicalCluster(ctx, workspacePath) + if err != nil { + return fmt.Errorf("failed to get client for workspace %s: %w", workspacePath, err) + } + + callerGroup := crossWorkspaceGatewayCallerGroup(gatewayHomeClusterID) + + clusterRole := &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: gatewayTokenReviewClusterRoleName}} + _, err = controllerutil.CreateOrUpdate(ctx, cl, clusterRole, func() error { + clusterRole.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{"authentication.k8s.io"}, + Resources: []string{"tokenreviews"}, + Verbs: []string{"create"}, + }, + } + if owner != nil { + return controllerutil.SetOwnerReference(owner, clusterRole, cl.Scheme()) + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to ensure gateway TokenReview ClusterRole in %s: %w", workspacePath, err) + } + + tokenReviewBinding := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: gatewayTokenReviewClusterRoleBindingName}} + _, err = controllerutil.CreateOrUpdate(ctx, cl, tokenReviewBinding, func() error { + tokenReviewBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: gatewayTokenReviewClusterRoleName, + } + tokenReviewBinding.Subjects = []rbacv1.Subject{callerGroup} + if owner != nil { + return controllerutil.SetOwnerReference(owner, tokenReviewBinding, cl.Scheme()) + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to ensure gateway TokenReview ClusterRoleBinding in %s: %w", workspacePath, err) + } + + workspaceAccessBinding := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: gatewayTokenReviewWorkspaceAccessBindingName}} + _, err = controllerutil.CreateOrUpdate(ctx, cl, workspaceAccessBinding, func() error { + workspaceAccessBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: gatewayTokenReviewWorkspaceAccessClusterRole, + } + workspaceAccessBinding.Subjects = []rbacv1.Subject{callerGroup} + if owner != nil { + return controllerutil.SetOwnerReference(owner, workspaceAccessBinding, cl.Scheme()) + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to ensure gateway workspace access ClusterRoleBinding in %s: %w", workspacePath, err) + } + + return nil +} + +func (r *tokenReviewRBACSubroutine) gatewayHomeClusterID(ctx context.Context) (string, error) { + if r.cachedGatewayHomeClusterID != "" { + return r.cachedGatewayHomeClusterID, nil + } + + id, err := clusterIDFromWorkspacePath(ctx, r.kcpClientGetter, gatewayHomeWorkspacePath) + if err != nil { + return "", err + } + + r.cachedGatewayHomeClusterID = id + return id, nil +} + +// crossWorkspaceGatewayCallerGroup is the kcp RBAC group for identities that +// originate from gatewayHomeClusterID and act in another workspace. +func crossWorkspaceGatewayCallerGroup(gatewayHomeClusterID string) rbacv1.Subject { + return rbacv1.Subject{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Group", + Name: fmt.Sprintf("system:cluster:%s", gatewayHomeClusterID), + } +} + +// clusterIDFromWorkspacePath reads kcp.io/cluster from the LogicalCluster named +// "cluster" in workspacePath (same contract as account_tuples and apiexportpolicy). +func clusterIDFromWorkspacePath(ctx context.Context, kcpClientGetter iclient.KCPClientGetter, workspacePath string) (string, error) { + cl, err := kcpClientGetter.NewClientForLogicalCluster(ctx, workspacePath) + if err != nil { + return "", fmt.Errorf("getting client for workspace %s: %w", workspacePath, err) + } + + var lc kcpcorev1alpha1.LogicalCluster + if err := cl.Get(ctx, client.ObjectKey{Name: "cluster"}, &lc); err != nil { + return "", fmt.Errorf("getting logical cluster for path %s: %w", workspacePath, err) + } + + clusterID, ok := lc.Annotations["kcp.io/cluster"] + if !ok || clusterID == "" { + return "", fmt.Errorf("kcp.io/cluster annotation not found on logical cluster %s", workspacePath) + } + + return clusterID, nil +} diff --git a/internal/subroutine/tokenreview_rbac_test.go b/internal/subroutine/tokenreview_rbac_test.go new file mode 100644 index 00000000..fd40dd54 --- /dev/null +++ b/internal/subroutine/tokenreview_rbac_test.go @@ -0,0 +1,228 @@ +package subroutine + +import ( + "context" + "testing" + + "github.com/platform-mesh/security-operator/internal/config" + "github.com/platform-mesh/security-operator/internal/subroutine/mocks" + "github.com/platform-mesh/subroutines" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" +) + +const testGatewayHomeClusterID = "gateway-home-cluster-id" + +func TestTokenReviewRBACSubroutine_GetName(t *testing.T) { + sub := NewTokenReviewRBACSubroutine(nil) + assert.Equal(t, "TokenReviewRBAC", sub.GetName()) +} + +func TestCrossWorkspaceGatewayCallerGroup(t *testing.T) { + subject := crossWorkspaceGatewayCallerGroup("abc123cluster") + assert.Equal(t, "Group", subject.Kind) + assert.Equal(t, "system:cluster:abc123cluster", subject.Name) +} + +func TestTokenReviewRBACSubroutine_reconcile_orgWorkspace_createsRBAC(t *testing.T) { + const orgPath = "root:orgs:org1" + lc := orgLogicalCluster(orgPath) + wsClient := newRBACFakeClient(t) + + kcpHelper := mocks.NewMockKCPClientGetter(t) + expectGatewayHomeClusterLookup(t, kcpHelper) + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, orgPath).Return(wsClient, nil).Once() + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, config.OrgsClusterPath).Return(wsClient, nil).Once() + + sub := NewTokenReviewRBACSubroutine(kcpHelper) + + result, err := sub.reconcile(context.Background(), lc) + require.NoError(t, err) + assert.Equal(t, subroutines.OK(), result) + + assertGatewayTokenReviewRBAC(t, wsClient, testGatewayHomeClusterID) +} + +func TestTokenReviewRBACSubroutine_reconcile_accountWorkspace_createsRBAC(t *testing.T) { + const accountPath = "root:orgs:org1:account1" + lc := orgLogicalCluster(accountPath) + wsClient := newRBACFakeClient(t) + + kcpHelper := mocks.NewMockKCPClientGetter(t) + expectGatewayHomeClusterLookup(t, kcpHelper) + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, accountPath).Return(wsClient, nil).Once() + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, config.OrgsClusterPath).Return(wsClient, nil).Once() + + sub := NewTokenReviewRBACSubroutine(kcpHelper) + + result, err := sub.reconcile(context.Background(), lc) + require.NoError(t, err) + assert.Equal(t, subroutines.OK(), result) + + assertGatewayTokenReviewRBAC(t, wsClient, testGatewayHomeClusterID) +} + +func TestTokenReviewRBACSubroutine_reconcile_nestedAccountWorkspace_createsRBAC(t *testing.T) { + const nestedAccountPath = "root:orgs:org1:account1:account2" + lc := orgLogicalCluster(nestedAccountPath) + wsClient := newRBACFakeClient(t) + + kcpHelper := mocks.NewMockKCPClientGetter(t) + expectGatewayHomeClusterLookup(t, kcpHelper) + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, nestedAccountPath).Return(wsClient, nil).Once() + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, config.OrgsClusterPath).Return(wsClient, nil).Once() + + sub := NewTokenReviewRBACSubroutine(kcpHelper) + + _, err := sub.reconcile(context.Background(), lc) + require.NoError(t, err) + assertGatewayTokenReviewRBAC(t, wsClient, testGatewayHomeClusterID) +} + +func TestTokenReviewRBACSubroutine_reconcile_atOrgsPath_skipsParentBindings(t *testing.T) { + lc := orgLogicalCluster(config.OrgsClusterPath) + wsClient := newRBACFakeClient(t) + + kcpHelper := mocks.NewMockKCPClientGetter(t) + expectGatewayHomeClusterLookup(t, kcpHelper) + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, config.OrgsClusterPath).Return(wsClient, nil).Once() + + sub := NewTokenReviewRBACSubroutine(kcpHelper) + + _, err := sub.reconcile(context.Background(), lc) + require.NoError(t, err) + assertGatewayTokenReviewRBAC(t, wsClient, testGatewayHomeClusterID) +} + +func TestTokenReviewRBACSubroutine_reconcile_missingPath(t *testing.T) { + lc := &kcpcorev1alpha1.LogicalCluster{ObjectMeta: metav1.ObjectMeta{Name: "cluster"}} + sub := NewTokenReviewRBACSubroutine(nil) + + _, err := sub.reconcile(context.Background(), lc) + require.ErrorContains(t, err, "no kcp.io/path annotation") +} + +func TestClusterIDFromWorkspacePath(t *testing.T) { + kcpHelper := mocks.NewMockKCPClientGetter(t) + homeClient := newRBACFakeClient(t) + require.NoError(t, homeClient.Create(context.Background(), gatewayHomeLogicalCluster(testGatewayHomeClusterID))) + + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, gatewayHomeWorkspacePath).Return(homeClient, nil).Once() + + id, err := clusterIDFromWorkspacePath(context.Background(), kcpHelper, gatewayHomeWorkspacePath) + require.NoError(t, err) + assert.Equal(t, testGatewayHomeClusterID, id) +} + +func TestClusterIDFromWorkspacePath_missingAnnotation(t *testing.T) { + kcpHelper := mocks.NewMockKCPClientGetter(t) + homeClient := newRBACFakeClient(t) + require.NoError(t, homeClient.Create(context.Background(), &kcpcorev1alpha1.LogicalCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Annotations: map[string]string{ + "kcp.io/path": gatewayHomeWorkspacePath, + }, + }, + })) + + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, gatewayHomeWorkspacePath).Return(homeClient, nil).Once() + + _, err := clusterIDFromWorkspacePath(context.Background(), kcpHelper, gatewayHomeWorkspacePath) + require.ErrorContains(t, err, "kcp.io/cluster annotation not found") +} + +func TestGatewayHomeClusterID_retriesAfterTransientFailure(t *testing.T) { + kcpHelper := mocks.NewMockKCPClientGetter(t) + failClient := newRBACFakeClient(t) + successClient := newRBACFakeClient(t) + require.NoError(t, successClient.Create(context.Background(), gatewayHomeLogicalCluster(testGatewayHomeClusterID))) + + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, gatewayHomeWorkspacePath).Return(failClient, nil).Once() + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, gatewayHomeWorkspacePath).Return(successClient, nil).Once() + + sub := NewTokenReviewRBACSubroutine(kcpHelper) + + _, err := sub.gatewayHomeClusterID(context.Background()) + require.Error(t, err) + + id, err := sub.gatewayHomeClusterID(context.Background()) + require.NoError(t, err) + assert.Equal(t, testGatewayHomeClusterID, id) + + // Successful lookup is cached; no further API calls. + id, err = sub.gatewayHomeClusterID(context.Background()) + require.NoError(t, err) + assert.Equal(t, testGatewayHomeClusterID, id) +} + +func orgLogicalCluster(path string) *kcpcorev1alpha1.LogicalCluster { + return &kcpcorev1alpha1.LogicalCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Annotations: map[string]string{ + "kcp.io/path": path, + }, + }, + } +} + +func gatewayHomeLogicalCluster(clusterID string) *kcpcorev1alpha1.LogicalCluster { + return &kcpcorev1alpha1.LogicalCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Annotations: map[string]string{ + "kcp.io/path": gatewayHomeWorkspacePath, + "kcp.io/cluster": clusterID, + }, + }, + } +} + +func expectGatewayHomeClusterLookup(t *testing.T, kcpHelper *mocks.MockKCPClientGetter) { + t.Helper() + homeClient := newRBACFakeClient(t) + require.NoError(t, homeClient.Create(context.Background(), gatewayHomeLogicalCluster(testGatewayHomeClusterID))) + kcpHelper.EXPECT().NewClientForLogicalCluster(mock.Anything, gatewayHomeWorkspacePath).Return(homeClient, nil).Once() +} + +func newRBACFakeClient(t *testing.T) client.Client { + t.Helper() + s := runtime.NewScheme() + require.NoError(t, scheme.AddToScheme(s)) + require.NoError(t, kcpcorev1alpha1.AddToScheme(s)) + require.NoError(t, rbacv1.AddToScheme(s)) + return fake.NewClientBuilder().WithScheme(s).Build() +} + +func assertGatewayTokenReviewRBAC(t *testing.T, wsClient client.Client, gatewayHomeClusterID string) { + t.Helper() + ctx := context.Background() + expectedGroup := "system:cluster:" + gatewayHomeClusterID + + var clusterRole rbacv1.ClusterRole + require.NoError(t, wsClient.Get(ctx, types.NamespacedName{Name: gatewayTokenReviewClusterRoleName}, &clusterRole)) + require.Len(t, clusterRole.Rules, 1) + assert.Equal(t, "tokenreviews", clusterRole.Rules[0].Resources[0]) + + var tokenReviewBinding rbacv1.ClusterRoleBinding + require.NoError(t, wsClient.Get(ctx, types.NamespacedName{Name: gatewayTokenReviewClusterRoleBindingName}, &tokenReviewBinding)) + require.Len(t, tokenReviewBinding.Subjects, 1) + assert.Equal(t, expectedGroup, tokenReviewBinding.Subjects[0].Name) + + var workspaceAccessBinding rbacv1.ClusterRoleBinding + require.NoError(t, wsClient.Get(ctx, types.NamespacedName{Name: gatewayTokenReviewWorkspaceAccessBindingName}, &workspaceAccessBinding)) + assert.Equal(t, gatewayTokenReviewWorkspaceAccessClusterRole, workspaceAccessBinding.RoleRef.Name) + assert.Equal(t, expectedGroup, workspaceAccessBinding.Subjects[0].Name) +}