diff --git a/pkg/registry/apis/iam/legacy/delete_team.sql b/pkg/registry/apis/iam/legacy/delete_team.sql new file mode 100644 index 00000000000..2e68fe1648d --- /dev/null +++ b/pkg/registry/apis/iam/legacy/delete_team.sql @@ -0,0 +1,2 @@ +DELETE FROM {{ .Ident .TeamTable }} +WHERE uid = {{ .Arg .Command.UID }} diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index d14bbb76123..3f2156d26a2 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -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) } diff --git a/pkg/registry/apis/iam/legacy/sql_test.go b/pkg/registry/apis/iam/legacy/sql_test.go index 4cfdcc35004..d9187c163b5 100644 --- a/pkg/registry/apis/iam/legacy/sql_test.go +++ b/pkg/registry/apis/iam/legacy/sql_test.go @@ -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", diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index e9bc80476eb..a52f7658df9 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -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 diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--delete_team-delete_team_basic.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--delete_team-delete_team_basic.sql new file mode 100755 index 00000000000..2263074103c --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--delete_team-delete_team_basic.sql @@ -0,0 +1,2 @@ +DELETE FROM `grafana`.`team` +WHERE uid = 'team-1' diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--delete_team-delete_team_basic.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--delete_team-delete_team_basic.sql new file mode 100755 index 00000000000..da4ea12930e --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--delete_team-delete_team_basic.sql @@ -0,0 +1,2 @@ +DELETE FROM "grafana"."team" +WHERE uid = 'team-1' diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--delete_team-delete_team_basic.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--delete_team-delete_team_basic.sql new file mode 100755 index 00000000000..da4ea12930e --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--delete_team-delete_team_basic.sql @@ -0,0 +1,2 @@ +DELETE FROM "grafana"."team" +WHERE uid = 'team-1' diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index f06c04bb955..88bf50986d6 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -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. diff --git a/pkg/tests/apis/iam/team_integration_test.go b/pkg/tests/apis/iam/team_integration_test.go index 5e0145dcede..f6e0a4605f6 100644 --- a/pkg/tests/apis/iam/team_integration_test.go +++ b/pkg/tests/apis/iam/team_integration_test.go @@ -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") }) }