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
This commit is contained in:
Mihai Doarna
2025-10-23 11:39:38 +03:00
committed by GitHub
parent 24d3c88c09
commit 3076f7a9b9
12 changed files with 592 additions and 4 deletions
+1
View File
@@ -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)
+16
View File
@@ -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",
@@ -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)
@@ -0,0 +1,4 @@
UPDATE `grafana`.`team_member`
SET permission = 'Admin',
updated = '2023-01-01 12:00:00'
WHERE uid = 'team-member-1'
@@ -0,0 +1,4 @@
UPDATE "grafana"."team_member"
SET permission = 'Admin',
updated = '2023-01-01 12:00:00'
WHERE uid = 'team-member-1'
@@ -0,0 +1,4 @@
UPDATE "grafana"."team_member"
SET permission = 'Admin',
updated = '2023-01-01 12:00:00'
WHERE uid = 'team-member-1'
@@ -0,0 +1,4 @@
UPDATE {{ .Ident .TeamMemberTable }}
SET permission = {{ .Arg .Command.Permission }},
updated = {{ .Arg .Command.Updated }}
WHERE uid = {{ .Arg .Command.UID }}
+6
View File
@@ -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:
+49 -1
View File
@@ -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) {
@@ -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
}
@@ -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)
})
}
}