IAM: Implement delete in team API (#111367)

* implement delete in team API

* add unit tests for legacy sql

* add integration tests

* remove comment

* remove org_id from delete team sql command

* fetch team ID before deleting it
This commit is contained in:
Mihai Doarna
2025-09-25 14:55:54 +03:00
committed by GitHub
parent 7b1413ec54
commit 1e4358163b
9 changed files with 142 additions and 3 deletions
@@ -0,0 +1,2 @@
DELETE FROM {{ .Ident .TeamTable }}
WHERE uid = {{ .Arg .Command.UID }}
+1
View File
@@ -32,6 +32,7 @@ type LegacyIdentityStore interface {
GetTeamInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetTeamInternalIDQuery) (*GetTeamInternalIDResult, error)
CreateTeam(ctx context.Context, ns claims.NamespaceInfo, cmd CreateTeamCommand) (*CreateTeamResult, error)
ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error)
DeleteTeam(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamCommand) error
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)
}
+14
View File
@@ -78,6 +78,12 @@ func TestIdentityQueries(t *testing.T) {
return &v
}
deleteTeam := func(q *DeleteTeamCommand) sqltemplate.SQLTemplate {
v := newDeleteTeam(nodb, q)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
listUserTeams := func(q *ListUserTeamsQuery) sqltemplate.SQLTemplate {
v := newListUserTeams(nodb, q)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
@@ -387,6 +393,14 @@ func TestIdentityQueries(t *testing.T) {
}),
},
},
sqlDeleteTeamTemplate: {
{
Name: "delete_team_basic",
Data: deleteTeam(&DeleteTeamCommand{
UID: "team-1",
}),
},
},
sqlCreateOrgUserTemplate: {
{
Name: "create_org_user_basic",
+63
View File
@@ -265,6 +265,69 @@ func (s *legacySQLStore) CreateTeam(ctx context.Context, ns claims.NamespaceInfo
return &CreateTeamResult{Team: createdTeam}, nil
}
type DeleteTeamCommand struct {
UID string
}
var sqlDeleteTeamTemplate = mustTemplate("delete_team.sql")
func newDeleteTeam(sql *legacysql.LegacyDatabaseHelper, cmd *DeleteTeamCommand) deleteTeamQuery {
return deleteTeamQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
TeamTable: sql.Table("team"),
Command: cmd,
}
}
type deleteTeamQuery struct {
sqltemplate.SQLTemplate
TeamTable string
Command *DeleteTeamCommand
}
func (r deleteTeamQuery) Validate() error {
return nil
}
func (s *legacySQLStore) DeleteTeam(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamCommand) error {
sql, err := s.sql(ctx)
if err != nil {
return err
}
req := newDeleteTeam(sql, &cmd)
if err := req.Validate(); err != nil {
return err
}
return sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error {
_, err := s.GetTeamInternalID(ctx, ns, GetTeamInternalIDQuery{
OrgID: ns.OrgID,
UID: cmd.UID,
})
if err != nil {
return err
}
teamDeleteReq := newDeleteTeam(sql, &cmd)
if err := teamDeleteReq.Validate(); err != nil {
return err
}
teamDeleteQuery, err := sqltemplate.Execute(sqlDeleteTeamTemplate, teamDeleteReq)
if err != nil {
return fmt.Errorf("error executing team delete template: %w", err)
}
_, err = st.Exec(ctx, teamDeleteQuery, teamDeleteReq.GetArgs()...)
if err != nil {
return fmt.Errorf("failed to delete team: %w", err)
}
return nil
})
}
type ListTeamBindingsQuery struct {
// UID is team uid to list bindings for. If not set store should list bindings for all teams
UID string
@@ -0,0 +1,2 @@
DELETE FROM `grafana`.`team`
WHERE uid = 'team-1'
@@ -0,0 +1,2 @@
DELETE FROM "grafana"."team"
WHERE uid = 'team-1'
@@ -0,0 +1,2 @@
DELETE FROM "grafana"."team"
WHERE uid = 'team-1'
+34 -1
View File
@@ -74,7 +74,40 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res
// Delete implements rest.GracefulDeleter.
func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete")
if !s.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete")
}
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
toBeDeleted, err := s.Get(ctx, name, nil)
if err != nil {
return nil, false, err
}
if deleteValidation != nil {
if err := deleteValidation(ctx, toBeDeleted); err != nil {
return nil, false, err
}
}
err = s.store.DeleteTeam(ctx, ns, legacy.DeleteTeamCommand{
UID: name,
})
if err != nil {
return nil, false, err
}
return &iamv0alpha1.Team{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns.Value,
},
}, true, nil
}
// Update implements rest.Updater.
+22 -2
View File
@@ -48,7 +48,7 @@ func TestIntegrationTeams(t *testing.T) {
}
func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) {
t.Run("should create team and get it using the new APIs as a GrafanaAdmin", func(t *testing.T) {
t.Run("should create/get/delete team using the new APIs as a GrafanaAdmin", func(t *testing.T) {
ctx := context.Background()
teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
@@ -80,6 +80,16 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) {
require.Equal(t, createdUID, fetched.GetName())
require.Equal(t, "default", fetched.GetNamespace())
err = teamClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{})
require.NoError(t, err)
_, err = teamClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
require.Error(t, err)
var statusErr *errors.StatusError
require.ErrorAs(t, err, &statusErr)
require.Equal(t, "Failure", statusErr.ErrStatus.Status)
require.Contains(t, statusErr.ErrStatus.Message, "team not found")
})
t.Run("should not be able to create team when using a user with insufficient permissions", func(t *testing.T) {
@@ -193,7 +203,7 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) {
}
func doTeamCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) {
t.Run("should create team using legacy APIs and get it using the new APIs", func(t *testing.T) {
t.Run("should create team using legacy APIs and get/delete it using the new APIs", func(t *testing.T) {
ctx := context.Background()
teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
@@ -232,5 +242,15 @@ func doTeamCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper)
require.Equal(t, rsp.Result.UID, team.GetName())
require.Equal(t, "default", team.GetNamespace())
err = teamClient.Resource.Delete(ctx, rsp.Result.UID, metav1.DeleteOptions{})
require.NoError(t, err)
_, err = teamClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{})
require.Error(t, err)
var statusErr *errors.StatusError
require.ErrorAs(t, err, &statusErr)
require.Equal(t, "Failure", statusErr.ErrStatus.Status)
require.Contains(t, statusErr.ErrStatus.Message, "team not found")
})
}