From 294712d7ef03bc23805e2767556dfe819d7b6227 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 30 Aug 2024 15:05:27 +0200 Subject: [PATCH] User: Add sub resource and api for user teams (#92649) * Add sub resource for user teams * Add test snapshots * Update to use ref:s --- pkg/apis/identity/v0alpha1/register.go | 1 + pkg/apis/identity/v0alpha1/types_team.go | 2 +- pkg/apis/identity/v0alpha1/types_user.go | 14 +++ .../v0alpha1/zz_generated.deepcopy.go | 48 +++++++++ .../identity/v0alpha1/zz_generated.openapi.go | 85 ++++++++++++++- ...enerated.openapi_violation_exceptions.list | 1 - pkg/registry/apis/identity/common/common.go | 11 ++ pkg/registry/apis/identity/legacy/sql.go | 4 +- pkg/registry/apis/identity/legacy/sql_test.go | 24 +++++ pkg/registry/apis/identity/legacy/team.go | 5 - ...user_teams_query-team_1_members_page_1.sql | 8 ++ ...user_teams_query-team_1_members_page_2.sql | 9 ++ ...user_teams_query-team_1_members_page_1.sql | 8 ++ ...user_teams_query-team_1_members_page_2.sql | 9 ++ ...user_teams_query-team_1_members_page_1.sql | 8 ++ ...user_teams_query-team_1_members_page_2.sql | 9 ++ pkg/registry/apis/identity/legacy/user.go | 93 ++++++++++++++++ .../apis/identity/legacy/user_teams_query.sql | 11 ++ pkg/registry/apis/identity/register.go | 4 +- .../apis/identity/team/rest_members.go | 2 - pkg/registry/apis/identity/team/store.go | 26 ----- .../apis/identity/team/store_binding.go | 2 +- .../apis/identity/team/store_user_team.go | 88 --------------- .../{display_store.go => rest_display.go} | 34 +++--- .../apis/identity/user/rest_user_team.go | 101 ++++++++++++++++++ pkg/tests/apis/identity/identity_test.go | 3 +- 26 files changed, 461 insertions(+), 149 deletions(-) create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_2.sql create mode 100644 pkg/registry/apis/identity/legacy/user_teams_query.sql delete mode 100644 pkg/registry/apis/identity/team/store_user_team.go rename pkg/registry/apis/identity/user/{display_store.go => rest_display.go} (79%) create mode 100644 pkg/registry/apis/identity/user/rest_user_team.go diff --git a/pkg/apis/identity/v0alpha1/register.go b/pkg/apis/identity/v0alpha1/register.go index ec327601a3a..ba97f4555e1 100644 --- a/pkg/apis/identity/v0alpha1/register.go +++ b/pkg/apis/identity/v0alpha1/register.go @@ -161,6 +161,7 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) { schema.GroupVersion{Group: GROUP, Version: version}, &User{}, &UserList{}, + &UserTeamList{}, &ServiceAccount{}, &ServiceAccountList{}, &Team{}, diff --git a/pkg/apis/identity/v0alpha1/types_team.go b/pkg/apis/identity/v0alpha1/types_team.go index 30fd1b97532..b4621401386 100644 --- a/pkg/apis/identity/v0alpha1/types_team.go +++ b/pkg/apis/identity/v0alpha1/types_team.go @@ -13,7 +13,7 @@ type Team struct { } type TeamSpec struct { - Title string `json:"name,omitempty"` + Title string `json:"title,omitempty"` Email string `json:"email,omitempty"` } diff --git a/pkg/apis/identity/v0alpha1/types_user.go b/pkg/apis/identity/v0alpha1/types_user.go index 4fc1f3448b7..713b2041876 100644 --- a/pkg/apis/identity/v0alpha1/types_user.go +++ b/pkg/apis/identity/v0alpha1/types_user.go @@ -25,3 +25,17 @@ type UserList struct { Items []User `json:"items,omitempty"` } + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type UserTeamList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []UserTeam `json:"items,omitempty"` +} + +type UserTeam struct { + Title string `json:"title,omitempty"` + TeamRef TeamRef `json:"teamRef,omitempty"` + Permission TeamPermission `json:"permission,omitempty"` +} diff --git a/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go index 23699951066..e4d9f5eaabf 100644 --- a/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go @@ -533,3 +533,51 @@ func (in *UserSpec) DeepCopy() *UserSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserTeam) DeepCopyInto(out *UserTeam) { + *out = *in + out.TeamRef = in.TeamRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserTeam. +func (in *UserTeam) DeepCopy() *UserTeam { + if in == nil { + return nil + } + out := new(UserTeam) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserTeamList) DeepCopyInto(out *UserTeamList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]UserTeam, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserTeamList. +func (in *UserTeamList) DeepCopy() *UserTeamList { + if in == nil { + return nil + } + out := new(UserTeamList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UserTeamList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/apis/identity/v0alpha1/zz_generated.openapi.go b/pkg/apis/identity/v0alpha1/zz_generated.openapi.go index 92c30b08e2f..3fee7aa66fe 100644 --- a/pkg/apis/identity/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/identity/v0alpha1/zz_generated.openapi.go @@ -35,6 +35,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.User": schema_pkg_apis_identity_v0alpha1_User(ref), "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserList": schema_pkg_apis_identity_v0alpha1_UserList(ref), "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserSpec": schema_pkg_apis_identity_v0alpha1_UserSpec(ref), + "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeam": schema_pkg_apis_identity_v0alpha1_UserTeam(ref), + "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeamList": schema_pkg_apis_identity_v0alpha1_UserTeamList(ref), } } @@ -763,7 +765,7 @@ func schema_pkg_apis_identity_v0alpha1_TeamSpec(ref common.ReferenceCallback) co SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "name": { + "title": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", @@ -936,3 +938,84 @@ func schema_pkg_apis_identity_v0alpha1_UserSpec(ref common.ReferenceCallback) co }, } } + +func schema_pkg_apis_identity_v0alpha1_UserTeam(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "teamRef": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef"), + }, + }, + "permission": { + SchemaProps: spec.SchemaProps{ + Description: "Possible enum values:\n - `\"admin\"`\n - `\"member\"`", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"admin", "member"}, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef"}, + } +} + +func schema_pkg_apis_identity_v0alpha1_UserTeamList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeam"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.UserTeam", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} diff --git a/pkg/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list index 429f6a92576..ed0d379d152 100644 --- a/pkg/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,4 +1,3 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,TeamBindingSpec,Subjects API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,IdentityDisplay,IdentityType API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,IdentityDisplay,InternalID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/identity/v0alpha1,TeamSpec,Title diff --git a/pkg/registry/apis/identity/common/common.go b/pkg/registry/apis/identity/common/common.go index a38c0107f0f..c696350570a 100644 --- a/pkg/registry/apis/identity/common/common.go +++ b/pkg/registry/apis/identity/common/common.go @@ -2,6 +2,9 @@ package common import ( "strconv" + + identityv0 "github.com/grafana/grafana/pkg/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/services/team" ) // OptonalFormatInt formats num as a string. If num is less or equal than 0 @@ -12,3 +15,11 @@ func OptionalFormatInt(num int64) string { } return "" } + +func MapTeamPermission(p team.PermissionType) identityv0.TeamPermission { + if p == team.PermissionTypeAdmin { + return identityv0.TeamPermissionAdmin + } else { + return identityv0.TeamPermissionMember + } +} diff --git a/pkg/registry/apis/identity/legacy/sql.go b/pkg/registry/apis/identity/legacy/sql.go index 10964349fbd..1957f0636c0 100644 --- a/pkg/registry/apis/identity/legacy/sql.go +++ b/pkg/registry/apis/identity/legacy/sql.go @@ -7,7 +7,6 @@ import ( "text/template" "github.com/grafana/authlib/claims" - "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/storage/legacysql" ) @@ -16,12 +15,11 @@ type LegacyIdentityStore interface { ListDisplay(ctx context.Context, ns claims.NamespaceInfo, query ListDisplayQuery) (*ListUserResult, error) ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error) + ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error) ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error) ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error) ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error) - - GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error) } var ( diff --git a/pkg/registry/apis/identity/legacy/sql_test.go b/pkg/registry/apis/identity/legacy/sql_test.go index 75b56710ff4..dc9812e1c45 100644 --- a/pkg/registry/apis/identity/legacy/sql_test.go +++ b/pkg/registry/apis/identity/legacy/sql_test.go @@ -48,6 +48,12 @@ func TestIdentityQueries(t *testing.T) { return &v } + listUserTeams := func(q *ListUserTeamsQuery) sqltemplate.SQLTemplate { + v := newListUserTeams(nodb, q) + v.SQLTemplate = mocks.NewTestingSQLTemplate() + return &v + } + mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ RootDir: "testdata", Templates: map[*template.Template][]mocks.TemplateTestCase{ @@ -168,6 +174,24 @@ func TestIdentityQueries(t *testing.T) { }), }, }, + sqlQueryUserTeamsTemplate: { + { + Name: "team_1_members_page_1", + Data: listUserTeams(&ListUserTeamsQuery{ + UserUID: "user-1", + OrgID: 1, + Pagination: common.Pagination{Limit: 1}, + }), + }, + { + Name: "team_1_members_page_2", + Data: listUserTeams(&ListUserTeamsQuery{ + UserUID: "user-1", + OrgID: 1, + Pagination: common.Pagination{Limit: 1, Continue: 2}, + }), + }, + }, }, }) } diff --git a/pkg/registry/apis/identity/legacy/team.go b/pkg/registry/apis/identity/legacy/team.go index 97cb14e23d8..27f12dd6c6a 100644 --- a/pkg/registry/apis/identity/legacy/team.go +++ b/pkg/registry/apis/identity/legacy/team.go @@ -328,8 +328,3 @@ func scanMember(rows *sql.Rows) (TeamMember, error) { err := rows.Scan(&m.ID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.UserID, &m.Name, &m.Email, &m.Username, &m.External, &m.Created, &m.Updated, &m.Permission) return m, err } - -// GetUserTeams implements LegacyIdentityStore. -func (s *legacySQLStore) GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error) { - panic("unimplemented") -} diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_1.sql new file mode 100755 index 00000000000..ae13941efd6 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_1.sql @@ -0,0 +1,8 @@ +SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission +FROM `grafana`.`user` u +INNER JOIN `grafana`.`team_member` tm on u.id = tm.user_id +INNER JOIN `grafana`.`team`t on tm.team_id = t.id +WHERE u.uid = 'user-1' +AND t.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_2.sql new file mode 100755 index 00000000000..40c8ea3ddfe --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--user_teams_query-team_1_members_page_2.sql @@ -0,0 +1,9 @@ +SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission +FROM `grafana`.`user` u +INNER JOIN `grafana`.`team_member` tm on u.id = tm.user_id +INNER JOIN `grafana`.`team`t on tm.team_id = t.id +WHERE u.uid = 'user-1' +AND t.org_id = 1 + AND t.id >= 2 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_1.sql new file mode 100755 index 00000000000..a685ac3d937 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_1.sql @@ -0,0 +1,8 @@ +SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission +FROM "grafana"."user" u +INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id +INNER JOIN "grafana"."team"t on tm.team_id = t.id +WHERE u.uid = 'user-1' +AND t.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_2.sql new file mode 100755 index 00000000000..e79621400e1 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--user_teams_query-team_1_members_page_2.sql @@ -0,0 +1,9 @@ +SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission +FROM "grafana"."user" u +INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id +INNER JOIN "grafana"."team"t on tm.team_id = t.id +WHERE u.uid = 'user-1' +AND t.org_id = 1 + AND t.id >= 2 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_1.sql new file mode 100755 index 00000000000..a685ac3d937 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_1.sql @@ -0,0 +1,8 @@ +SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission +FROM "grafana"."user" u +INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id +INNER JOIN "grafana"."team"t on tm.team_id = t.id +WHERE u.uid = 'user-1' +AND t.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_2.sql new file mode 100755 index 00000000000..e79621400e1 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--user_teams_query-team_1_members_page_2.sql @@ -0,0 +1,9 @@ +SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission +FROM "grafana"."user" u +INNER JOIN "grafana"."team_member" tm on u.id = tm.user_id +INNER JOIN "grafana"."team"t on tm.team_id = t.id +WHERE u.uid = 'user-1' +AND t.org_id = 1 + AND t.id >= 2 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/user.go b/pkg/registry/apis/identity/legacy/user.go index 7f5a8494940..ba5f71312fe 100644 --- a/pkg/registry/apis/identity/legacy/user.go +++ b/pkg/registry/apis/identity/legacy/user.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/registry/apis/identity/common" + "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" @@ -109,3 +110,95 @@ func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDa return res, err } + +type ListUserTeamsQuery struct { + UserUID string + OrgID int64 + Pagination common.Pagination +} + +type ListUserTeamsResult struct { + Continue int64 + Items []UserTeam +} + +type UserTeam struct { + ID int64 + UID string + Name string + Permission team.PermissionType +} + +var sqlQueryUserTeamsTemplate = mustTemplate("user_teams_query.sql") + +func newListUserTeams(sql *legacysql.LegacyDatabaseHelper, q *ListUserTeamsQuery) listUserTeamsQuery { + return listUserTeamsQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + UserTable: sql.Table("user"), + TeamTable: sql.Table("team"), + TeamMemberTable: sql.Table("team_member"), + Query: q, + } +} + +type listUserTeamsQuery struct { + sqltemplate.SQLTemplate + Query *ListUserTeamsQuery + UserTable string + TeamTable string + TeamMemberTable string +} + +func (r listUserTeamsQuery) Validate() error { + return nil +} + +func (s *legacySQLStore) ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error) { + query.Pagination.Limit += 1 + query.OrgID = ns.OrgID + if query.OrgID == 0 { + return nil, fmt.Errorf("expected non zero org id") + } + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + req := newListUserTeams(sql, &query) + q, err := sqltemplate.Execute(sqlQueryUserTeamsTemplate, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeamsTemplate.Name(), err) + } + + rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + + if err != nil { + return nil, err + } + + res := &ListUserTeamsResult{} + var lastID int64 + for rows.Next() { + t := UserTeam{} + err := rows.Scan(&t.ID, &t.UID, &t.Name, &t.Permission) + if err != nil { + return nil, err + } + + lastID = t.ID + res.Items = append(res.Items, t) + if len(res.Items) > int(query.Pagination.Limit)-1 { + res.Continue = lastID + res.Items = res.Items[0 : len(res.Items)-1] + break + } + } + + return res, err +} diff --git a/pkg/registry/apis/identity/legacy/user_teams_query.sql b/pkg/registry/apis/identity/legacy/user_teams_query.sql new file mode 100644 index 00000000000..6925999962a --- /dev/null +++ b/pkg/registry/apis/identity/legacy/user_teams_query.sql @@ -0,0 +1,11 @@ +SELECT t.id as team_id, t.uid as team_uid, t.name as team_name, tm.permission +FROM {{ .Ident .UserTable }} u +INNER JOIN {{ .Ident .TeamMemberTable }} tm on u.id = tm.user_id +INNER JOIN {{ .Ident .TeamTable }}t on tm.team_id = t.id +WHERE u.uid = {{ .Arg .Query.UserUID }} +AND t.org_id = {{ .Arg .Query.OrgID }} +{{- if .Query.Pagination.Continue }} + AND t.id >= {{ .Arg .Query.Pagination.Continue }} +{{- end }} +ORDER BY t.id ASC +LIMIT {{ .Arg .Query.Pagination.Limit }}; diff --git a/pkg/registry/apis/identity/register.go b/pkg/registry/apis/identity/register.go index 2bc2b5b0a20..94287ecc6fb 100644 --- a/pkg/registry/apis/identity/register.go +++ b/pkg/registry/apis/identity/register.go @@ -89,7 +89,7 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo( userResource := identityv0.UserResourceInfo storage[userResource.StoragePath()] = user.NewLegacyStore(b.Store) - storage[userResource.StoragePath("teams")] = team.NewLegacyUserTeamsStore(b.Store) + storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.Store) serviceaccountResource := identityv0.ServiceAccountResourceInfo storage[serviceaccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.Store) @@ -100,7 +100,7 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo( } // The display endpoint -- NOTE, this uses a rewrite hack to allow requests without a name parameter - storage["display"] = user.NewLegacyDisplayStore(b.Store) + storage["display"] = user.NewLegacyDisplayREST(b.Store) apiGroupInfo.VersionedResourcesStorageMap[identityv0.VERSION] = storage return &apiGroupInfo, nil diff --git a/pkg/registry/apis/identity/team/rest_members.go b/pkg/registry/apis/identity/team/rest_members.go index 2279c489235..e18eaa20146 100644 --- a/pkg/registry/apis/identity/team/rest_members.go +++ b/pkg/registry/apis/identity/team/rest_members.go @@ -62,8 +62,6 @@ func (s *LegacyTeamMemberREST) Connect(ctx context.Context, name string, options } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _ = common.PaginationFromListQuery(r.URL.Query()) - res, err := s.store.ListTeamMembers(ctx, ns, legacy.ListTeamMembersQuery{ UID: name, Pagination: common.PaginationFromListQuery(r.URL.Query()), diff --git a/pkg/registry/apis/identity/team/store.go b/pkg/registry/apis/identity/team/store.go index 06ef7769936..a18b4a9c082 100644 --- a/pkg/registry/apis/identity/team/store.go +++ b/pkg/registry/apis/identity/team/store.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/identity/common" "github.com/grafana/grafana/pkg/registry/apis/identity/legacy" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/team" ) var ( @@ -130,28 +129,3 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO } return nil, resource.NewNotFound(name) } - -func asTeam(team *team.Team, ns string) (*identityv0.Team, error) { - item := &identityv0.Team{ - ObjectMeta: metav1.ObjectMeta{ - Name: team.UID, - Namespace: ns, - CreationTimestamp: metav1.NewTime(team.Created), - ResourceVersion: strconv.FormatInt(team.Updated.UnixMilli(), 10), - }, - Spec: identityv0.TeamSpec{ - Title: team.Name, - Email: team.Email, - }, - } - meta, err := utils.MetaAccessor(item) - if err != nil { - return nil, err - } - meta.SetUpdatedTimestamp(&team.Updated) - meta.SetOriginInfo(&utils.ResourceOriginInfo{ - Name: "SQL", - Path: strconv.FormatInt(team.ID, 10), - }) - return item, nil -} diff --git a/pkg/registry/apis/identity/team/store_binding.go b/pkg/registry/apis/identity/team/store_binding.go index 6c1abc8593b..f3499ff82a3 100644 --- a/pkg/registry/apis/identity/team/store_binding.go +++ b/pkg/registry/apis/identity/team/store_binding.go @@ -150,7 +150,7 @@ func mapToSubjects(members []legacy.TeamMember) []identityv0.TeamSubject { for _, m := range members { out = append(out, identityv0.TeamSubject{ Name: m.MemberID(), - Permission: mapPermisson(m.Permission), + Permission: common.MapTeamPermission(m.Permission), }) } return out diff --git a/pkg/registry/apis/identity/team/store_user_team.go b/pkg/registry/apis/identity/team/store_user_team.go deleted file mode 100644 index 2d9128a5e33..00000000000 --- a/pkg/registry/apis/identity/team/store_user_team.go +++ /dev/null @@ -1,88 +0,0 @@ -package team - -import ( - "context" - "net/http" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - identityv0 "github.com/grafana/grafana/pkg/apis/identity/v0alpha1" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/registry/apis/identity/legacy" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" -) - -var ( - _ rest.Storage = (*LegacyUserTeamsStore)(nil) - _ rest.SingularNameProvider = (*LegacyUserTeamsStore)(nil) - _ rest.Connecter = (*LegacyUserTeamsStore)(nil) - _ rest.Scoper = (*LegacyUserTeamsStore)(nil) - _ rest.StorageMetadata = (*LegacyUserTeamsStore)(nil) -) - -func NewLegacyUserTeamsStore(store legacy.LegacyIdentityStore) *LegacyUserTeamsStore { - return &LegacyUserTeamsStore{ - logger: log.New("user teams"), - store: store, - } -} - -type LegacyUserTeamsStore struct { - logger log.Logger - store legacy.LegacyIdentityStore -} - -func (r *LegacyUserTeamsStore) New() runtime.Object { - return &identityv0.TeamList{} -} - -func (r *LegacyUserTeamsStore) Destroy() {} - -func (r *LegacyUserTeamsStore) NamespaceScoped() bool { - return true -} - -func (r *LegacyUserTeamsStore) GetSingularName() string { - return "TeamList" -} - -func (r *LegacyUserTeamsStore) ProducesMIMETypes(verb string) []string { - return []string{"application/json"} -} - -func (r *LegacyUserTeamsStore) ProducesObject(verb string) interface{} { - return &identityv0.TeamList{} -} - -func (r *LegacyUserTeamsStore) ConnectMethods() []string { - return []string{"GET"} -} - -func (r *LegacyUserTeamsStore) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" // true means you can use the trailing path as a variable -} - -func (r *LegacyUserTeamsStore) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) { - ns, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - teams, err := r.store.GetUserTeams(ctx, ns, name) - if err != nil { - return nil, err - } - - return http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - list := &identityv0.TeamList{} - for _, team := range teams { - t, err := asTeam(&team, ns.Value) - if err != nil { - responder.Error(err) - return - } - list.Items = append(list.Items, *t) - } - responder.Object(200, list) - }), nil -} diff --git a/pkg/registry/apis/identity/user/display_store.go b/pkg/registry/apis/identity/user/rest_display.go similarity index 79% rename from pkg/registry/apis/identity/user/display_store.go rename to pkg/registry/apis/identity/user/rest_display.go index 76a7c780890..c5d5ecf0202 100644 --- a/pkg/registry/apis/identity/user/display_store.go +++ b/pkg/registry/apis/identity/user/rest_display.go @@ -18,57 +18,57 @@ import ( "k8s.io/apiserver/pkg/registry/rest" ) -type LegacyDisplayStore struct { +type LegacyDisplayREST struct { store legacy.LegacyIdentityStore } var ( - _ rest.Storage = (*LegacyDisplayStore)(nil) - _ rest.SingularNameProvider = (*LegacyDisplayStore)(nil) - _ rest.Connecter = (*LegacyDisplayStore)(nil) - _ rest.Scoper = (*LegacyDisplayStore)(nil) - _ rest.StorageMetadata = (*LegacyDisplayStore)(nil) + _ rest.Storage = (*LegacyDisplayREST)(nil) + _ rest.SingularNameProvider = (*LegacyDisplayREST)(nil) + _ rest.Connecter = (*LegacyDisplayREST)(nil) + _ rest.Scoper = (*LegacyDisplayREST)(nil) + _ rest.StorageMetadata = (*LegacyDisplayREST)(nil) ) -func NewLegacyDisplayStore(store legacy.LegacyIdentityStore) *LegacyDisplayStore { - return &LegacyDisplayStore{store} +func NewLegacyDisplayREST(store legacy.LegacyIdentityStore) *LegacyDisplayREST { + return &LegacyDisplayREST{store} } -func (r *LegacyDisplayStore) New() runtime.Object { +func (r *LegacyDisplayREST) New() runtime.Object { return &identity.IdentityDisplayResults{} } -func (r *LegacyDisplayStore) Destroy() {} +func (r *LegacyDisplayREST) Destroy() {} -func (r *LegacyDisplayStore) NamespaceScoped() bool { +func (r *LegacyDisplayREST) NamespaceScoped() bool { return true } -func (r *LegacyDisplayStore) GetSingularName() string { +func (r *LegacyDisplayREST) GetSingularName() string { // not actually used anywhere, but required by SingularNameProvider return "identitydisplay" } -func (r *LegacyDisplayStore) ProducesMIMETypes(verb string) []string { +func (r *LegacyDisplayREST) ProducesMIMETypes(verb string) []string { return []string{"application/json"} } -func (r *LegacyDisplayStore) ProducesObject(verb string) any { +func (r *LegacyDisplayREST) ProducesObject(verb string) any { return &identity.IdentityDisplayResults{} } -func (r *LegacyDisplayStore) ConnectMethods() []string { +func (r *LegacyDisplayREST) ConnectMethods() []string { return []string{"GET"} } -func (r *LegacyDisplayStore) NewConnectOptions() (runtime.Object, bool, string) { +func (r *LegacyDisplayREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" // true means you can use the trailing path as a variable } // This will always have an empty app url var fakeCfgForGravatar = &setting.Cfg{} -func (r *LegacyDisplayStore) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) { +func (r *LegacyDisplayREST) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) { // See: /pkg/services/apiserver/builder/helper.go#L34 // The name is set with a rewriter hack if name != "name" { diff --git a/pkg/registry/apis/identity/user/rest_user_team.go b/pkg/registry/apis/identity/user/rest_user_team.go new file mode 100644 index 00000000000..5235dcc3592 --- /dev/null +++ b/pkg/registry/apis/identity/user/rest_user_team.go @@ -0,0 +1,101 @@ +package user + +import ( + "context" + "net/http" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + identityv0 "github.com/grafana/grafana/pkg/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/identity/common" + "github.com/grafana/grafana/pkg/registry/apis/identity/legacy" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" +) + +var ( + _ rest.Storage = (*LegacyUserTeamREST)(nil) + _ rest.Scoper = (*LegacyUserTeamREST)(nil) + _ rest.StorageMetadata = (*LegacyUserTeamREST)(nil) + _ rest.Connecter = (*LegacyUserTeamREST)(nil) +) + +func NewLegacyTeamMemberREST(store legacy.LegacyIdentityStore) *LegacyUserTeamREST { + return &LegacyUserTeamREST{store} +} + +type LegacyUserTeamREST struct { + store legacy.LegacyIdentityStore +} + +// New implements rest.Storage. +func (s *LegacyUserTeamREST) New() runtime.Object { + return &identityv0.UserTeamList{} +} + +// Destroy implements rest.Storage. +func (s *LegacyUserTeamREST) Destroy() {} + +// NamespaceScoped implements rest.Scoper. +func (s *LegacyUserTeamREST) NamespaceScoped() bool { + return true +} + +// ProducesMIMETypes implements rest.StorageMetadata. +func (s *LegacyUserTeamREST) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +// ProducesObject implements rest.StorageMetadata. +func (s *LegacyUserTeamREST) ProducesObject(verb string) interface{} { + return s.New() +} + +// Connect implements rest.Connecter. +func (s *LegacyUserTeamREST) Connect(ctx context.Context, name string, options runtime.Object, responder rest.Responder) (http.Handler, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + res, err := s.store.ListUserTeams(ctx, ns, legacy.ListUserTeamsQuery{ + UserUID: name, + Pagination: common.PaginationFromListQuery(r.URL.Query()), + }) + if err != nil { + responder.Error(err) + return + } + + list := &identityv0.UserTeamList{Items: make([]identityv0.UserTeam, 0, len(res.Items))} + + for _, m := range res.Items { + list.Items = append(list.Items, mapToUserTeam(m)) + } + + list.ListMeta.Continue = common.OptionalFormatInt(res.Continue) + + responder.Object(http.StatusOK, list) + }), nil +} + +// NewConnectOptions implements rest.Connecter. +func (s *LegacyUserTeamREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +// ConnectMethods implements rest.Connecter. +func (s *LegacyUserTeamREST) ConnectMethods() []string { + return []string{http.MethodGet} +} + +func mapToUserTeam(t legacy.UserTeam) identityv0.UserTeam { + return identityv0.UserTeam{ + Title: t.Name, + TeamRef: identityv0.TeamRef{ + Name: t.UID, + }, + Permission: common.MapTeamPermission(t.Permission), + } +} diff --git a/pkg/tests/apis/identity/identity_test.go b/pkg/tests/apis/identity/identity_test.go index a98858e8ea3..d945c27f8d5 100644 --- a/pkg/tests/apis/identity/identity_test.go +++ b/pkg/tests/apis/identity/identity_test.go @@ -69,7 +69,6 @@ func TestIntegrationIdentity(t *testing.T) { rsp, err := teamClient.Resource.List(ctx, metav1.ListOptions{}) require.NoError(t, err) found := teamClient.SanitizeJSONList(rsp, "name") - // fmt.Printf("%s", found) require.JSONEq(t, `{ "items": [ { @@ -87,7 +86,7 @@ func TestIntegrationIdentity(t *testing.T) { }, "spec": { "email": "staff@Org1", - "name": "staff" + "title": "staff" } } ]