From c9a14f177459da2375302c23032aec1d61c31916 Mon Sep 17 00:00:00 2001 From: Misi Date: Tue, 13 Jan 2026 14:45:18 +0100 Subject: [PATCH] IAM: Target resource authorization for TeamBinding (#116117) * wip * Review VerbGet vs VerbGetPermissions * Fix tests --- pkg/registry/apis/iam/authorizer.go | 2 +- .../iam/authorizer/team_binding_authorizer.go | 156 +++++++++++ .../team_binding_authorizer_test.go | 253 ++++++++++++++++++ pkg/registry/apis/iam/register.go | 12 +- .../iam/team_bindings_integration_test.go | 126 +++++---- .../testdata/teambinding-test-create-v0.yaml | 2 +- 6 files changed, 496 insertions(+), 55 deletions(-) create mode 100644 pkg/registry/apis/iam/authorizer/team_binding_authorizer.go create mode 100644 pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index efcf6fa5b39..da81dd2d9a8 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -42,7 +42,6 @@ func newIAMAuthorizer( // Identity specific resources legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient) - resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer resourceAuthorizer["display"] = legacyAuthorizer // Access specific resources @@ -55,6 +54,7 @@ func newIAMAuthorizer( resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer resourceAuthorizer[iamv0.ExternalGroupMappingResourceInfo.GetName()] = allowAuthorizer resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = authorizer + resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = allowAuthorizer resourceAuthorizer["searchUsers"] = serviceAuthorizer resourceAuthorizer["searchTeams"] = serviceAuthorizer diff --git a/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go b/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go new file mode 100644 index 00000000000..2a4f5ae5e51 --- /dev/null +++ b/pkg/registry/apis/iam/authorizer/team_binding_authorizer.go @@ -0,0 +1,156 @@ +package authorizer + +import ( + "context" + "fmt" + + "github.com/grafana/authlib/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper" +) + +type TeamBindingAuthorizer struct { + accessClient types.AccessClient +} + +var _ storewrapper.ResourceStorageAuthorizer = (*TeamBindingAuthorizer)(nil) + +func NewTeamBindingAuthorizer( + accessClient types.AccessClient, +) *TeamBindingAuthorizer { + return &TeamBindingAuthorizer{ + accessClient: accessClient, + } +} + +// AfterGet implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error { + authInfo, ok := types.AuthInfoFrom(ctx) + if !ok { + return storewrapper.ErrUnauthenticated + } + + concreteObj, ok := obj.(*iamv0.TeamBinding) + if !ok { + return apierrors.NewInternalError(fmt.Errorf("expected TeamBinding, got %T: %w", obj, storewrapper.ErrUnexpectedType)) + } + + // Accesscontrol should check on the TeamResourceInfo group resource if the user can use VerbGetPermissions + // on the team (TeamRef.Name) (handled below) OR if the subject's name (TeamBindingSpec.Subject.Name) is equal to the current Identity's UID/Identifier. + if concreteObj.Spec.Subject.Name == authInfo.GetIdentifier() { + return nil + } + + teamName := concreteObj.Spec.TeamRef.Name + checkReq := types.CheckRequest{ + Namespace: authInfo.GetNamespace(), + Group: iamv0.TeamResourceInfo.GroupResource().Group, + Resource: iamv0.TeamResourceInfo.GroupResource().Resource, + Verb: utils.VerbGetPermissions, + Name: teamName, + } + res, err := r.accessClient.Check(ctx, authInfo, checkReq, "") + if err != nil { + return apierrors.NewInternalError(err) + } + + if !res.Allowed { + return apierrors.NewForbidden( + iamv0.TeamBindingResourceInfo.GroupResource(), + concreteObj.Name, + fmt.Errorf("user cannot access team %s", teamName), + ) + } + return nil +} + +// BeforeCreate implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error { + return r.beforeWrite(ctx, obj) +} + +// BeforeDelete implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error { + return r.beforeWrite(ctx, obj) +} + +// BeforeUpdate implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error { + return r.beforeWrite(ctx, obj) +} + +func (r *TeamBindingAuthorizer) beforeWrite(ctx context.Context, obj runtime.Object) error { + authInfo, ok := types.AuthInfoFrom(ctx) + if !ok { + return storewrapper.ErrUnauthenticated + } + + concreteObj, ok := obj.(*iamv0.TeamBinding) + if !ok { + return apierrors.NewInternalError(fmt.Errorf("expected TeamBinding, got %T: %w", obj, storewrapper.ErrUnexpectedType)) + } + + teamName := concreteObj.Spec.TeamRef.Name + checkReq := types.CheckRequest{ + Namespace: authInfo.GetNamespace(), + Group: iamv0.GROUP, + Resource: iamv0.TeamResourceInfo.GetName(), + Verb: utils.VerbSetPermissions, + Name: teamName, + } + + res, err := r.accessClient.Check(ctx, authInfo, checkReq, "") + if err != nil { + return apierrors.NewInternalError(err) + } + + if !res.Allowed { + return apierrors.NewForbidden( + iamv0.TeamBindingResourceInfo.GroupResource(), + concreteObj.Name, + fmt.Errorf("user cannot write team %s", teamName), + ) + } + return nil +} + +// FilterList implements ResourceStorageAuthorizer. +func (r *TeamBindingAuthorizer) FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error) { + authInfo, ok := types.AuthInfoFrom(ctx) + if !ok { + return nil, storewrapper.ErrUnauthenticated + } + + l, ok := list.(*iamv0.TeamBindingList) + if !ok { + return nil, apierrors.NewInternalError(fmt.Errorf("expected TeamBindingList, got %T: %w", list, storewrapper.ErrUnexpectedType)) + } + + var filteredItems []iamv0.TeamBinding + + listReq := types.ListRequest{ + Namespace: authInfo.GetNamespace(), + Group: iamv0.TeamResourceInfo.GroupResource().Group, + Resource: iamv0.TeamResourceInfo.GroupResource().Resource, + Verb: utils.VerbGetPermissions, + } + canView, _, err := r.accessClient.Compile(ctx, authInfo, listReq) + if err != nil { + return nil, apierrors.NewInternalError(err) + } + + for _, item := range l.Items { + // Accesscontrol should check on the TeamResourceInfo group resource if the user can use VerbGetPermissions + // on the team (TeamRef.Name) OR if the subject's name (TeamBindingSpec.Subject.Name) is equal to the current Identity's UID/Identifier. + if item.Spec.Subject.Name == authInfo.GetIdentifier() || canView(item.Spec.TeamRef.Name, "") { + filteredItems = append(filteredItems, item) + } + } + + l.Items = filteredItems + return l, nil +} diff --git a/pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go b/pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go new file mode 100644 index 00000000000..f3bf8795de1 --- /dev/null +++ b/pkg/registry/apis/iam/authorizer/team_binding_authorizer_test.go @@ -0,0 +1,253 @@ +package authorizer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/authlib/types" + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +func newTeamBinding(teamName, name, subjectName string) *iamv0.TeamBinding { + return &iamv0.TeamBinding{ + ObjectMeta: metav1.ObjectMeta{Namespace: "org-2", Name: name}, + Spec: iamv0.TeamBindingSpec{ + TeamRef: iamv0.TeamBindingTeamRef{ + Name: teamName, + }, + Subject: iamv0.TeamBindingspecSubject{ + Name: subjectName, + }, + }, + } +} + +func TestTeamBinding_AfterGet(t *testing.T) { + tests := []struct { + name string + teamBinding *iamv0.TeamBinding + shouldAllow bool + checkCalled bool + }{ + { + name: "allow access via permission", + teamBinding: newTeamBinding("team-1", "binding-1", "other"), + shouldAllow: true, + checkCalled: true, + }, + { + name: "deny access", + teamBinding: newTeamBinding("team-1", "binding-1", "other"), + shouldAllow: false, + checkCalled: true, // called but returns allowed=false + }, + { + name: "allow access via subject match", + teamBinding: newTeamBinding("team-1", "binding-1", "u001"), + shouldAllow: true, + checkCalled: false, // short-circuits + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.NotNil(t, id) + require.Equal(t, "u001", id.GetIdentifier()) + + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbGetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.AfterGet(ctx, tt.teamBinding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.Equal(t, tt.checkCalled, accessClient.checkCalled) + }) + } +} + +func TestTeamBinding_FilterList(t *testing.T) { + list := &iamv0.TeamBindingList{ + Items: []iamv0.TeamBinding{ + *newTeamBinding("team-1", "binding-1", "other"), // Access via permission + *newTeamBinding("team-2", "binding-2", "other"), // No access + *newTeamBinding("team-3", "binding-3", "u001"), // Access via subject match + }, + } + + compileFunc := func(id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) { + require.NotNil(t, id) + require.Equal(t, "u001", id.GetIdentifier()) + + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GroupResource().Resource, req.Resource) + require.Equal(t, utils.VerbGetPermissions, req.Verb) + + return func(name, folder string) bool { + return name == "team-1" + }, &types.NoopZookie{}, nil + } + + accessClient := &fakeAccessClient{compileFunc: compileFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + obj, err := authz.FilterList(ctx, list) + require.NoError(t, err) + require.NotNil(t, list) + require.True(t, accessClient.compileCalled) + + filtered, ok := obj.(*iamv0.TeamBindingList) + require.True(t, ok) + require.Len(t, filtered.Items, 2) + + names := []string{filtered.Items[0].Name, filtered.Items[1].Name} + require.Contains(t, names, "binding-1") + require.Contains(t, names, "binding-3") +} + +func TestTeamBinding_BeforeCreate(t *testing.T) { + binding := newTeamBinding("team-1", "binding-1", "other") + + tests := []struct { + name string + shouldAllow bool + }{ + { + name: "allow create", + shouldAllow: true, + }, + { + name: "deny create", + shouldAllow: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbSetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.BeforeCreate(ctx, binding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.True(t, accessClient.checkCalled) + }) + } +} + +func TestTeamBinding_BeforeUpdate(t *testing.T) { + binding := newTeamBinding("team-1", "binding-1", "other") + + tests := []struct { + name string + shouldAllow bool + }{ + { + name: "allow update", + shouldAllow: true, + }, + { + name: "deny update", + shouldAllow: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbSetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.BeforeUpdate(ctx, binding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.True(t, accessClient.checkCalled) + }) + } +} + +func TestTeamBinding_BeforeDelete(t *testing.T) { + binding := newTeamBinding("team-1", "binding-1", "other") + + tests := []struct { + name string + shouldAllow bool + }{ + { + name: "allow delete", + shouldAllow: true, + }, + { + name: "deny delete", + shouldAllow: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) { + require.Equal(t, "org-2", req.Namespace) + require.Equal(t, iamv0.GROUP, req.Group) + require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource) + require.Equal(t, "team-1", req.Name) + require.Equal(t, utils.VerbSetPermissions, req.Verb) + + return types.CheckResponse{Allowed: tt.shouldAllow}, nil + } + + accessClient := &fakeAccessClient{checkFunc: checkFunc} + authz := NewTeamBindingAuthorizer(accessClient) + ctx := types.WithAuthInfo(context.Background(), user) + + err := authz.BeforeDelete(ctx, binding) + if tt.shouldAllow { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.True(t, accessClient.checkCalled) + }) + } +} diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 7f42d620987..895f1d0bf22 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -361,7 +361,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts bui if err != nil { return err } - storage[teamBindingResource.StoragePath()] = teamBindingUniStore + var teamBindingStore storewrapper.K8sStorage = teamBindingUniStore // Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks if enableZanzanaSync { @@ -376,8 +376,16 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts bui if err != nil { return err } - storage[teamBindingResource.StoragePath()] = dw + + var ok bool + teamBindingStore, ok = dw.(storewrapper.K8sStorage) + if !ok { + return fmt.Errorf("expected storewrapper.K8sStorage, got %T", dw) + } } + + authzWrapper := storewrapper.New(teamBindingStore, iamauthorizer.NewTeamBindingAuthorizer(b.accessClient)) + storage[teamBindingResource.StoragePath()] = authzWrapper return nil } diff --git a/pkg/tests/apis/iam/team_bindings_integration_test.go b/pkg/tests/apis/iam/team_bindings_integration_test.go index 1b355296486..40258edaf45 100644 --- a/pkg/tests/apis/iam/team_bindings_integration_test.go +++ b/pkg/tests/apis/iam/team_bindings_integration_test.go @@ -67,7 +67,7 @@ func TestIntegrationTeamBindings(t *testing.T) { doTeamBindingCRUDTestsUsingTheNewAPIs(t, helper, team, user) if mode < 3 { - doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper, mode) + doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper) } }) } @@ -84,13 +84,15 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel }) // Create the team binding - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() + createdSpec := created.Object["spec"].(map[string]interface{}) require.Equal(t, user.GetName(), createdSpec["subject"].(map[string]interface{})["name"]) require.Equal(t, team.GetName(), createdSpec["teamRef"].(map[string]interface{})["name"]) @@ -115,6 +117,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel // Update the team binding toUpdate := toCreate.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member" + toUpdate.Object["metadata"].(map[string]interface{})["name"] = createdUID updated, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.NoError(t, err) require.NotNil(t, updated) @@ -164,9 +167,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.Error(t, err) @@ -185,9 +186,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = "" - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, "", team.GetName()) _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.Error(t, err) @@ -205,9 +204,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = "" + toCreate := createTeamBindingObject(helper, user.GetName(), "") _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) require.Error(t, err) @@ -225,9 +222,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) toCreate.Object["spec"].(map[string]interface{})["permission"] = "invalid" _, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) @@ -245,17 +240,31 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel } { t.Run(fmt.Sprintf("with basic role_%s", u.Identity.GetOrgRole()), func(t *testing.T) { ctx := context.Background() + + // Create the team binding using admin + adminClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrTeamBindings, + }) + + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := adminClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = adminClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: u, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrTeamBindings, }) - toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + toUpdate := created.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError @@ -273,10 +282,8 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") + toUpdate := createTeamBindingObject(helper, user.GetName(), team.GetName()) toUpdate.Object["metadata"].(map[string]interface{})["name"] = "invalid-team-binding-name" - toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError @@ -293,15 +300,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() toUpdate := toCreate.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = "test-team-2" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -317,16 +327,19 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() toUpdate := toCreate.DeepCopy() + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = "test-user-2" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -342,15 +355,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() toUpdate := toCreate.DeepCopy() toUpdate.Object["spec"].(map[string]interface{})["external"] = true - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -366,17 +382,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel GVR: gvrTeamBindings, }) - // Create the team binding if it doesn't already exist - toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() - _, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName()) + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) - toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") - toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() - toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + defer func() { + _ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) + }() + + toUpdate := createTeamBindingObject(helper, user.GetName(), team.GetName()) toUpdate.Object["spec"].(map[string]interface{})["permission"] = "invalid" - _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName() + _, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) @@ -385,7 +402,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel }) } -func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) { +func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) { t.Run("should create team binding using legacy APIs and get it using the new APIs", func(t *testing.T) { ctx := context.Background() @@ -499,3 +516,10 @@ func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTest require.Equal(t, teamBindingName, teamBinding.GetName()) }) } + +func createTeamBindingObject(helper *apis.K8sTestHelper, userName, teamName string) *unstructured.Unstructured { + obj := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") + obj.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = userName + obj.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = teamName + return obj +} diff --git a/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml b/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml index da04e7785b1..2ac36c1b6a6 100644 --- a/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml +++ b/pkg/tests/apis/iam/testdata/teambinding-test-create-v0.yaml @@ -1,7 +1,7 @@ apiVersion: iam.grafana.app/v0alpha1 kind: TeamBinding metadata: - name: test-team-binding-1 + generateName: test-team-binding- spec: subject: name: ""