From 3076f7a9b97fe378c11b39598faa0ccb2a1bccb2 Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Thu, 23 Oct 2025 11:39:38 +0300 Subject: [PATCH] IAM: Implement the update method for team bindings (#112788) * implement the update method for team bindings * fix lint error * add integration tests * add integration test for non existing team binding * try to fix lint error --- pkg/registry/apis/iam/legacy/sql.go | 1 + pkg/registry/apis/iam/legacy/sql_test.go | 16 ++ pkg/registry/apis/iam/legacy/team_binding.go | 67 +++++ ..._member_query-update_team_member_basic.sql | 4 + ..._member_query-update_team_member_basic.sql | 4 + ..._member_query-update_team_member_basic.sql | 4 + .../iam/legacy/update_team_member_query.sql | 4 + pkg/registry/apis/iam/register.go | 6 + pkg/registry/apis/iam/teambinding/store.go | 50 +++- pkg/registry/apis/iam/teambinding/validate.go | 25 ++ .../apis/iam/teambinding/validate_test.go | 239 ++++++++++++++++++ .../iam/team_bindings_integration_test.go | 176 ++++++++++++- 12 files changed, 592 insertions(+), 4 deletions(-) create mode 100755 pkg/registry/apis/iam/legacy/testdata/mysql--update_team_member_query-update_team_member_basic.sql create mode 100755 pkg/registry/apis/iam/legacy/testdata/postgres--update_team_member_query-update_team_member_basic.sql create mode 100755 pkg/registry/apis/iam/legacy/testdata/sqlite--update_team_member_query-update_team_member_basic.sql create mode 100644 pkg/registry/apis/iam/legacy/update_team_member_query.sql diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index 7a6ec18a561..df6f9911645 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -37,6 +37,7 @@ type LegacyIdentityStore interface { CreateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd CreateTeamMemberCommand) (*CreateTeamMemberResult, error) ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error) ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error) + UpdateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamMemberCommand) (*UpdateTeamMemberResult, error) } var _ LegacyIdentityStore = (*legacySQLStore)(nil) diff --git a/pkg/registry/apis/iam/legacy/sql_test.go b/pkg/registry/apis/iam/legacy/sql_test.go index 7f05e614475..4ad7d87c12b 100644 --- a/pkg/registry/apis/iam/legacy/sql_test.go +++ b/pkg/registry/apis/iam/legacy/sql_test.go @@ -85,6 +85,12 @@ func TestIdentityQueries(t *testing.T) { return &v } + updateTeamMember := func(cmd *UpdateTeamMemberCommand) sqltemplate.SQLTemplate { + v := newUpdateTeamMember(nodb, cmd) + v.SQLTemplate = mocks.NewTestingSQLTemplate() + return &v + } + listTeamMembers := func(q *ListTeamMembersQuery) sqltemplate.SQLTemplate { v := newListTeamMembers(nodb, q) v.SQLTemplate = mocks.NewTestingSQLTemplate() @@ -260,6 +266,16 @@ func TestIdentityQueries(t *testing.T) { }), }, }, + sqlUpdateTeamMemberQuery: { + { + Name: "update_team_member_basic", + Data: updateTeamMember(&UpdateTeamMemberCommand{ + UID: "team-member-1", + Permission: team.PermissionTypeAdmin, + Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)), + }), + }, + }, sqlQueryTeamMembersTemplate: { { Name: "team_1_members_page_1", diff --git a/pkg/registry/apis/iam/legacy/team_binding.go b/pkg/registry/apis/iam/legacy/team_binding.go index 7a4bce5a6b5..e3b89a8e0a9 100644 --- a/pkg/registry/apis/iam/legacy/team_binding.go +++ b/pkg/registry/apis/iam/legacy/team_binding.go @@ -301,6 +301,73 @@ func (s *legacySQLStore) ListTeamMembers(ctx context.Context, ns claims.Namespac return res, err } +type UpdateTeamMemberCommand struct { + UID string + Permission team.PermissionType + Updated legacysql.DBTime +} + +type UpdateTeamMemberResult struct { + UID string + Permission team.PermissionType + Updated legacysql.DBTime +} + +var sqlUpdateTeamMemberQuery = mustTemplate("update_team_member_query.sql") + +func newUpdateTeamMember(sql *legacysql.LegacyDatabaseHelper, cmd *UpdateTeamMemberCommand) updateTeamMemberQuery { + return updateTeamMemberQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + TeamMemberTable: sql.Table("team_member"), + Command: cmd, + } +} + +type updateTeamMemberQuery struct { + sqltemplate.SQLTemplate + TeamMemberTable string + Command *UpdateTeamMemberCommand +} + +func (r updateTeamMemberQuery) Validate() error { + return nil +} + +func (s *legacySQLStore) UpdateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamMemberCommand) (*UpdateTeamMemberResult, error) { + now := time.Now().UTC() + cmd.Updated = legacysql.NewDBTime(now) + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + req := newUpdateTeamMember(sql, &cmd) + + var result UpdateTeamMemberResult + err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error { + teamMemberQuery, err := sqltemplate.Execute(sqlUpdateTeamMemberQuery, req) + if err != nil { + return fmt.Errorf("failed to execute team member template %q: %w", sqlUpdateTeamMemberQuery.Name(), err) + } + + _, err = st.Exec(ctx, teamMemberQuery, req.GetArgs()...) + if err != nil { + return fmt.Errorf("failed to update team member: %w", err) + } + + result = UpdateTeamMemberResult(cmd) + + return nil + }) + + if err != nil { + return nil, err + } + + return &result, nil +} + func scanMember(rows *sql.Rows) (TeamMember, error) { m := TeamMember{} err := rows.Scan(&m.ID, &m.UID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.UserID, &m.Name, &m.Email, &m.Username, &m.External, &m.Created, &m.Updated, &m.Permission) diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--update_team_member_query-update_team_member_basic.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--update_team_member_query-update_team_member_basic.sql new file mode 100755 index 00000000000..a2469311c6b --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--update_team_member_query-update_team_member_basic.sql @@ -0,0 +1,4 @@ +UPDATE `grafana`.`team_member` +SET permission = 'Admin', + updated = '2023-01-01 12:00:00' +WHERE uid = 'team-member-1' diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--update_team_member_query-update_team_member_basic.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--update_team_member_query-update_team_member_basic.sql new file mode 100755 index 00000000000..9f194a916f1 --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--update_team_member_query-update_team_member_basic.sql @@ -0,0 +1,4 @@ +UPDATE "grafana"."team_member" +SET permission = 'Admin', + updated = '2023-01-01 12:00:00' +WHERE uid = 'team-member-1' diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--update_team_member_query-update_team_member_basic.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--update_team_member_query-update_team_member_basic.sql new file mode 100755 index 00000000000..9f194a916f1 --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--update_team_member_query-update_team_member_basic.sql @@ -0,0 +1,4 @@ +UPDATE "grafana"."team_member" +SET permission = 'Admin', + updated = '2023-01-01 12:00:00' +WHERE uid = 'team-member-1' diff --git a/pkg/registry/apis/iam/legacy/update_team_member_query.sql b/pkg/registry/apis/iam/legacy/update_team_member_query.sql new file mode 100644 index 00000000000..54f1c2fc6ea --- /dev/null +++ b/pkg/registry/apis/iam/legacy/update_team_member_query.sql @@ -0,0 +1,4 @@ +UPDATE {{ .Ident .TeamMemberTable }} +SET permission = {{ .Arg .Command.Permission }}, + updated = {{ .Arg .Command.Updated }} +WHERE uid = {{ .Arg .Command.UID }} diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index ed0222d01bd..599dc0d9c2b 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -407,6 +407,12 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm return fmt.Errorf("expected old object to be a Team, got %T", oldTeamObj) } return team.ValidateOnUpdate(ctx, typedObj, oldTeamObj) + case *iamv0.TeamBinding: + oldTeamBindingObj, ok := a.GetOldObject().(*iamv0.TeamBinding) + if !ok { + return fmt.Errorf("expected old object to be a TeamBinding, got %T", oldTeamBindingObj) + } + return teambinding.ValidateOnUpdate(ctx, typedObj, oldTeamBindingObj) } return nil case admission.Delete: diff --git a/pkg/registry/apis/iam/teambinding/store.go b/pkg/registry/apis/iam/teambinding/store.go index d53d6385da5..c1895558131 100644 --- a/pkg/registry/apis/iam/teambinding/store.go +++ b/pkg/registry/apis/iam/teambinding/store.go @@ -73,7 +73,55 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime. } func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update") + if !l.enableAuthnMutation { + return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update") + } + + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + oldObj, err := l.Get(ctx, name, nil) + if err != nil { + return oldObj, false, err + } + + obj, err := objInfo.UpdatedObject(ctx, oldObj) + if err != nil { + return oldObj, false, err + } + + teamBindingObj, ok := obj.(*iamv0alpha1.TeamBinding) + if !ok { + return nil, false, fmt.Errorf("expected TeamBinding object, got %T", obj) + } + + if updateValidation != nil { + if err := updateValidation(ctx, obj, oldObj); err != nil { + return oldObj, false, err + } + } + + var permission team.PermissionType + switch teamBindingObj.Spec.Permission { + case iamv0alpha1.TeamBindingTeamPermissionAdmin: + permission = team.PermissionTypeAdmin + case iamv0alpha1.TeamBindingTeamPermissionMember: + permission = team.PermissionTypeMember + } + + updateCmd := legacy.UpdateTeamMemberCommand{ + UID: teamBindingObj.Name, + Permission: permission, + } + + _, err = l.store.UpdateTeamMember(ctx, ns, updateCmd) + if err != nil { + return oldObj, false, err + } + + return teamBindingObj, false, nil } func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { diff --git a/pkg/registry/apis/iam/teambinding/validate.go b/pkg/registry/apis/iam/teambinding/validate.go index 27289ac19d2..f5ddf981be1 100644 --- a/pkg/registry/apis/iam/teambinding/validate.go +++ b/pkg/registry/apis/iam/teambinding/validate.go @@ -29,3 +29,28 @@ func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.TeamBinding) error { return nil } + +func ValidateOnUpdate(ctx context.Context, obj, old *iamv0alpha1.TeamBinding) error { + _, err := identity.GetRequester(ctx) + if err != nil { + return apierrors.NewUnauthorized("no identity found") + } + + if obj.Spec.TeamRef.Name != old.Spec.TeamRef.Name { + return apierrors.NewBadRequest("teamRef is immutable") + } + + if obj.Spec.Subject.Name != old.Spec.Subject.Name { + return apierrors.NewBadRequest("subject is immutable") + } + + if obj.Spec.External != old.Spec.External { + return apierrors.NewBadRequest("external is immutable") + } + + if obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionAdmin && obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionMember { + return apierrors.NewBadRequest("invalid permission") + } + + return nil +} diff --git a/pkg/registry/apis/iam/teambinding/validate_test.go b/pkg/registry/apis/iam/teambinding/validate_test.go index fbe4063fb43..cd0c1b9e130 100644 --- a/pkg/registry/apis/iam/teambinding/validate_test.go +++ b/pkg/registry/apis/iam/teambinding/validate_test.go @@ -121,3 +121,242 @@ func TestValidateOnCreate(t *testing.T) { }) } } + +func TestValidateOnUpdate(t *testing.T) { + tests := []struct { + name string + requester *identity.StaticRequester + old *iamv0alpha1.TeamBinding + obj *iamv0alpha1.TeamBinding + want error + }{ + { + name: "valid update - permission change", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + old: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionMember, + External: false, + }, + }, + obj: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + want: nil, + }, + { + name: "valid update - no changes", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + old: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + obj: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + want: nil, + }, + { + name: "invalid update - teamRef change", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + old: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + obj: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team-updated", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + want: apierrors.NewBadRequest("teamRef is immutable"), + }, + { + name: "invalid update - subject change", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + old: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + obj: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user-updated", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + want: apierrors.NewBadRequest("subject is immutable"), + }, + { + name: "invalid update - external change", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + old: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + obj: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: true, + }, + }, + want: apierrors.NewBadRequest("external is immutable"), + }, + { + name: "invalid update - invalid permission", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + old: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + obj: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: "invalid", + }, + }, + want: apierrors.NewBadRequest("invalid permission"), + }, + { + name: "invalid update - no requester in context", + requester: nil, + old: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + obj: &iamv0alpha1.TeamBinding{ + Spec: iamv0alpha1.TeamBindingSpec{ + Subject: iamv0alpha1.TeamBindingspecSubject{ + Name: "test-user", + }, + TeamRef: iamv0alpha1.TeamBindingTeamRef{ + Name: "test-team", + }, + Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin, + External: false, + }, + }, + want: apierrors.NewUnauthorized("no identity found"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := identity.WithRequester(context.Background(), test.requester) + err := ValidateOnUpdate(ctx, test.obj, test.old) + assert.Equal(t, test.want, err) + }) + } +} diff --git a/pkg/tests/apis/iam/team_bindings_integration_test.go b/pkg/tests/apis/iam/team_bindings_integration_test.go index d5532256d7a..669a69d6e3a 100644 --- a/pkg/tests/apis/iam/team_bindings_integration_test.go +++ b/pkg/tests/apis/iam/team_bindings_integration_test.go @@ -74,7 +74,7 @@ func TestIntegrationTeamBindings(t *testing.T) { } func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper, team *unstructured.Unstructured, user *unstructured.Unstructured) { - t.Run("should create/get team binding using the new APIs", func(t *testing.T) { + t.Run("should create/update/get team binding using the new APIs", func(t *testing.T) { ctx := context.Background() teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ @@ -110,9 +110,33 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel require.Equal(t, team.GetName(), fetchedSpec["teamRef"].(map[string]interface{})["name"]) require.Equal(t, "admin", fetchedSpec["permission"]) require.Equal(t, false, fetchedSpec["external"]) - require.Equal(t, createdUID, fetched.GetName()) - require.Equal(t, "default", fetched.GetNamespace()) + + // Update the team binding + toUpdate := toCreate.DeepCopy() + toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member" + updated, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, updated) + + updatedSpec := updated.Object["spec"].(map[string]interface{}) + require.Equal(t, createdUID, updated.GetName()) + require.Equal(t, user.GetName(), updatedSpec["subject"].(map[string]interface{})["name"]) + require.Equal(t, team.GetName(), updatedSpec["teamRef"].(map[string]interface{})["name"]) + require.Equal(t, "member", updatedSpec["permission"]) + require.Equal(t, false, updatedSpec["external"]) + + // Get the team binding + fetched, err = teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, fetched) + + fetchedSpec = fetched.Object["spec"].(map[string]interface{}) + require.Equal(t, user.GetName(), fetchedSpec["subject"].(map[string]interface{})["name"]) + require.Equal(t, team.GetName(), fetchedSpec["teamRef"].(map[string]interface{})["name"]) + require.Equal(t, "member", fetchedSpec["permission"]) + require.Equal(t, false, fetchedSpec["external"]) + require.Equal(t, createdUID, fetched.GetName()) }) t.Run("should not be able to create team binding when using a user with insufficient permissions", func(t *testing.T) { @@ -201,6 +225,152 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel require.Equal(t, int32(400), statusErr.ErrStatus.Code) require.Contains(t, statusErr.ErrStatus.Message, "invalid permission") }) + + t.Run("should not be able to update team binding with insufficient permissions", func(t *testing.T) { + for _, u := range []apis.User{ + helper.Org1.Editor, + helper.Org1.Viewer, + } { + t.Run(fmt.Sprintf("with basic role_%s", u.Identity.GetOrgRole()), func(t *testing.T) { + ctx := context.Background() + 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.Object["spec"].(map[string]interface{})["permission"] = "member" + _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + require.Error(t, err) + + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(403), statusErr.ErrStatus.Code) + }) + } + }) + + t.Run("should not be able to update team binding if the team binding does not exist", func(t *testing.T) { + ctx := context.Background() + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrTeamBindings, + }) + + toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") + 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 + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(404), statusErr.ErrStatus.Code) + require.Contains(t, statusErr.ErrStatus.Message, "not found") + }) + + t.Run("should not be able to update team binding with teamRef change", func(t *testing.T) { + ctx := context.Background() + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + 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{}) + + 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{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(400), statusErr.ErrStatus.Code) + require.Contains(t, statusErr.ErrStatus.Message, "teamRef is immutable") + }) + + t.Run("should not be able to update team binding with subject change", func(t *testing.T) { + ctx := context.Background() + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + 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{}) + + toUpdate := toCreate.DeepCopy() + toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = "test-user-2" + + _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(400), statusErr.ErrStatus.Code) + require.Contains(t, statusErr.ErrStatus.Message, "subject is immutable") + }) + + t.Run("should not be able to update team binding with external change", func(t *testing.T) { + ctx := context.Background() + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + 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{}) + + toUpdate := toCreate.DeepCopy() + toUpdate.Object["spec"].(map[string]interface{})["external"] = true + _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(400), statusErr.ErrStatus.Code) + require.Contains(t, statusErr.ErrStatus.Message, "external is immutable") + }) + + t.Run("should not be able to update team binding with invalid permission", func(t *testing.T) { + ctx := context.Background() + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + 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{}) + + 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.Object["spec"].(map[string]interface{})["permission"] = "invalid" + _, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{}) + require.Error(t, err) + var statusErr *errors.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, int32(400), statusErr.ErrStatus.Code) + require.Contains(t, statusErr.ErrStatus.Message, "invalid permission") + }) } func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) {