diff --git a/pkg/apis/identity/v0alpha1/register.go b/pkg/apis/identity/v0alpha1/register.go index 68c82f71969..ec327601a3a 100644 --- a/pkg/apis/identity/v0alpha1/register.go +++ b/pkg/apis/identity/v0alpha1/register.go @@ -170,6 +170,7 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) { &SSOSettingList{}, &TeamBinding{}, &TeamBindingList{}, + &TeamMemberList{}, ) } diff --git a/pkg/apis/identity/v0alpha1/types_identity.go b/pkg/apis/identity/v0alpha1/types_identity.go index 3ad562f2315..f0735958851 100644 --- a/pkg/apis/identity/v0alpha1/types_identity.go +++ b/pkg/apis/identity/v0alpha1/types_identity.go @@ -23,11 +23,19 @@ type IdentityDisplayResults struct { } type IdentityDisplay struct { - IdentityType claims.IdentityType `json:"type"` // The namespaced UID, eg `user|api-key|...` - UID string `json:"uid"` // The namespaced UID, eg `xyz` - Display string `json:"display"` - AvatarURL string `json:"avatarURL,omitempty"` + // Type of identity e.g. "user". + // For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24 + IdentityType claims.IdentityType `json:"type"` - // Legacy internal ID -- usage of this value should be phased out + // UID for identity, is a unique value for the type within a namespace. + UID string `json:"uid"` + + // Display name for identity. + Display string `json:"display"` + + // AvatarURL is the url where we can get the avatar for identity + AvatarURL string `json:"avatarURL,omitempty"` + + // InternalID is the legacy numreric id for identity, this is deprecated and should be phased out InternalID int64 `json:"internalId,omitempty"` } diff --git a/pkg/apis/identity/v0alpha1/types_team.go b/pkg/apis/identity/v0alpha1/types_team.go index 3a70bd8c36f..30fd1b97532 100644 --- a/pkg/apis/identity/v0alpha1/types_team.go +++ b/pkg/apis/identity/v0alpha1/types_team.go @@ -50,8 +50,7 @@ type TeamSubject struct { // Name is the unique identifier for subject. Name string `json:"name,omitempty"` - // Permission subject has in permission. - // Can be either admin or member. + // Permission subject has in team. Permission TeamPermission `json:"permission,omitempty"` } @@ -60,6 +59,23 @@ type TeamRef struct { Name string `json:"name,omitempty"` } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TeamMemberList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []TeamMember `json:"items,omitempty"` +} + +type TeamMember struct { + IdentityDisplay `json:",inline"` + + // External is set if member ship was synced from external IDP. + External bool `json:"external,omitempty"` + // Permission member has in team. + Permission TeamPermission `json:"permission,omitempty"` +} + // TeamPermission for subject // +enum type TeamPermission string diff --git a/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go index 1e74f720214..23699951066 100644 --- a/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/identity/v0alpha1/zz_generated.deepcopy.go @@ -362,6 +362,54 @@ func (in *TeamList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TeamMember) DeepCopyInto(out *TeamMember) { + *out = *in + out.IdentityDisplay = in.IdentityDisplay + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamMember. +func (in *TeamMember) DeepCopy() *TeamMember { + if in == nil { + return nil + } + out := new(TeamMember) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TeamMemberList) DeepCopyInto(out *TeamMemberList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TeamMember, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamMemberList. +func (in *TeamMemberList) DeepCopy() *TeamMemberList { + if in == nil { + return nil + } + out := new(TeamMemberList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TeamMemberList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TeamRef) DeepCopyInto(out *TeamRef) { *out = *in diff --git a/pkg/apis/identity/v0alpha1/zz_generated.openapi.go b/pkg/apis/identity/v0alpha1/zz_generated.openapi.go index 0c71545ef9b..92c30b08e2f 100644 --- a/pkg/apis/identity/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/identity/v0alpha1/zz_generated.openapi.go @@ -27,6 +27,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingList": schema_pkg_apis_identity_v0alpha1_TeamBindingList(ref), "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingSpec": schema_pkg_apis_identity_v0alpha1_TeamBindingSpec(ref), "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamList": schema_pkg_apis_identity_v0alpha1_TeamList(ref), + "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamMember": schema_pkg_apis_identity_v0alpha1_TeamMember(ref), + "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamMemberList": schema_pkg_apis_identity_v0alpha1_TeamMemberList(ref), "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef": schema_pkg_apis_identity_v0alpha1_TeamRef(ref), "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSpec": schema_pkg_apis_identity_v0alpha1_TeamSpec(ref), "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSubject": schema_pkg_apis_identity_v0alpha1_TeamSubject(ref), @@ -44,14 +46,15 @@ func schema_pkg_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallb Properties: map[string]spec.Schema{ "type": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "uid": { SchemaProps: spec.SchemaProps{ - Description: "The namespaced UID, eg `user|api-key|...`", + Description: "UID for identity, is a unique value for the type within a namespace.", Default: "", Type: []string{"string"}, Format: "", @@ -59,7 +62,7 @@ func schema_pkg_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallb }, "display": { SchemaProps: spec.SchemaProps{ - Description: "The namespaced UID, eg `xyz`", + Description: "Display name for identity.", Default: "", Type: []string{"string"}, Format: "", @@ -67,13 +70,14 @@ func schema_pkg_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallb }, "avatarURL": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Description: "AvatarURL is the url where we can get the avatar for identity", + Type: []string{"string"}, + Format: "", }, }, "internalId": { SchemaProps: spec.SchemaProps{ - Description: "Legacy internal ID -- usage of this value should be phased out", + Description: "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out", Type: []string{"integer"}, Format: "int64", }, @@ -621,6 +625,119 @@ func schema_pkg_apis_identity_v0alpha1_TeamList(ref common.ReferenceCallback) co } } +func schema_pkg_apis_identity_v0alpha1_TeamMember(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "UID for identity, is a unique value for the type within a namespace.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "display": { + SchemaProps: spec.SchemaProps{ + Description: "Display name for identity.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "avatarURL": { + SchemaProps: spec.SchemaProps{ + Description: "AvatarURL is the url where we can get the avatar for identity", + Type: []string{"string"}, + Format: "", + }, + }, + "internalId": { + SchemaProps: spec.SchemaProps{ + Description: "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "external": { + SchemaProps: spec.SchemaProps{ + Description: "External is set if member ship was synced from external IDP.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "permission": { + SchemaProps: spec.SchemaProps{ + Description: "Permission member has in team.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"admin", "member"}, + }, + }, + }, + Required: []string{"type", "uid", "display"}, + }, + }, + } +} + +func schema_pkg_apis_identity_v0alpha1_TeamMemberList(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.TeamMember"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamMember", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + func schema_pkg_apis_identity_v0alpha1_TeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -679,7 +796,7 @@ func schema_pkg_apis_identity_v0alpha1_TeamSubject(ref common.ReferenceCallback) }, "permission": { SchemaProps: spec.SchemaProps{ - Description: "Permission subject has in permission. Can be either admin or member.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`", + Description: "Permission subject has in team.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`", Type: []string{"string"}, Format: "", Enum: []interface{}{"admin", "member"}, diff --git a/pkg/registry/apis/folders/sub_access.go b/pkg/registry/apis/folders/sub_access.go index 3e7319f88a0..b0b2cf7f546 100644 --- a/pkg/registry/apis/folders/sub_access.go +++ b/pkg/registry/apis/folders/sub_access.go @@ -53,6 +53,7 @@ func (r *subAccessREST) Connect(ctx context.Context, name string, opts runtime.O if err != nil { return nil, err } + // Can view is managed here (and in the Authorizer) f, err := r.service.Get(ctx, &folder.GetFolderQuery{ UID: &name, diff --git a/pkg/registry/apis/identity/common/common.go b/pkg/registry/apis/identity/common/common.go index 25d7066db7e..a38c0107f0f 100644 --- a/pkg/registry/apis/identity/common/common.go +++ b/pkg/registry/apis/identity/common/common.go @@ -1,25 +1,9 @@ package common import ( - "fmt" "strconv" - - "k8s.io/apimachinery/pkg/apis/meta/internalversion" ) -// GetContinueID is a helper to parse options.Continue as int64. -// If no continue token is provided 0 is returned. -func GetContinueID(options *internalversion.ListOptions) (int64, error) { - if options.Continue != "" { - continueID, err := strconv.ParseInt(options.Continue, 10, 64) - if err != nil { - return 0, fmt.Errorf("invalid continue token: %w", err) - } - return continueID, nil - } - return 0, nil -} - // OptonalFormatInt formats num as a string. If num is less or equal than 0 // an empty string is returned. func OptionalFormatInt(num int64) string { diff --git a/pkg/registry/apis/identity/common/pagination.go b/pkg/registry/apis/identity/common/pagination.go new file mode 100644 index 00000000000..28b4d9694c0 --- /dev/null +++ b/pkg/registry/apis/identity/common/pagination.go @@ -0,0 +1,48 @@ +package common + +import ( + "net/url" + "strconv" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" +) + +type Pagination struct { + Limit int64 + Continue int64 +} + +func PaginationFromListOptions(options *internalversion.ListOptions) Pagination { + limit := options.Limit + if limit < 1 { + limit = 50 + } + + return Pagination{ + Limit: limit, + Continue: parseIntWithFallback(options.Continue, 0, 0), + } +} + +func PaginationFromListQuery(query url.Values) Pagination { + return Pagination{ + Limit: parseIntWithFallback(query.Get("limit"), 1, 50), + Continue: parseIntWithFallback(query.Get("continue"), 0, 0), + } +} + +func parseIntWithFallback(original string, min int64, fallback int64) int64 { + if original == "" { + return fallback + } + v, err := strconv.ParseInt(original, 10, 64) + if err != nil { + return fallback + } + + if v < min { + return fallback + } + + return v +} diff --git a/pkg/registry/apis/identity/legacy/legacy_sql.go b/pkg/registry/apis/identity/legacy/legacy_sql.go index 7380d0c4da9..a1deea114f9 100644 --- a/pkg/registry/apis/identity/legacy/legacy_sql.go +++ b/pkg/registry/apis/identity/legacy/legacy_sql.go @@ -2,6 +2,7 @@ package legacy import ( "context" + "database/sql" "fmt" "text/template" @@ -28,12 +29,8 @@ type legacySQLStore struct { // ListTeams implements LegacyIdentityStore. func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error) { - if query.Limit < 1 { - query.Limit = 50 - } - - limit := int(query.Limit) - query.Limit += 1 // for continue + // for continue + query.Pagination.Limit += 1 query.OrgID = ns.OrgID if ns.OrgID == 0 { return nil, fmt.Errorf("expected non zero orgID") @@ -45,13 +42,11 @@ func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo, } req := newListTeams(sql, &query) - rawQuery, err := sqltemplate.Execute(sqlQueryTeams, req) + q, err := sqltemplate.Execute(sqlQueryTeams, req) if err != nil { return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeams.Name(), err) } - q := rawQuery - res := &ListTeamResult{} rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) defer func() { if rows != nil { @@ -59,37 +54,41 @@ func (s *legacySQLStore) ListTeams(ctx context.Context, ns claims.NamespaceInfo, } }() - if err == nil { - // id, uid, name, email, created, updated - var lastID int64 - for rows.Next() { - t := team.Team{} - err = rows.Scan(&t.ID, &t.UID, &t.Name, &t.Email, &t.Created, &t.Updated) - if err != nil { - return res, err - } - lastID = t.ID - res.Teams = append(res.Teams, t) - if len(res.Teams) > limit { - res.ContinueID = lastID - break - } + res := &ListTeamResult{} + if err != nil { + return nil, err + } + + var lastID int64 + for rows.Next() { + t := team.Team{} + err = rows.Scan(&t.ID, &t.UID, &t.Name, &t.Email, &t.Created, &t.Updated) + if err != nil { + return res, err } - if query.UID == "" { - res.RV, err = sql.GetResourceVersion(ctx, "team", "updated") + + lastID = t.ID + res.Teams = append(res.Teams, t) + if len(res.Teams) > int(query.Pagination.Limit)-1 { + res.Teams = res.Teams[0 : len(res.Teams)-1] + res.Continue = lastID + break } } + + if query.UID == "" { + res.RV, err = sql.GetResourceVersion(ctx, "team", "updated") + } + return res, err } // ListUsers implements LegacyIdentityStore. func (s *legacySQLStore) ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error) { - if query.Limit < 1 { - query.Limit = 50 - } + // for continue + limit := int(query.Pagination.Limit) + query.Pagination.Limit += 1 - limit := int(query.Limit) - query.Limit += 1 // for continue query.OrgID = ns.OrgID if ns.OrgID == 0 { return nil, fmt.Errorf("expected non zero orgID") @@ -101,10 +100,10 @@ func (s *legacySQLStore) ListUsers(ctx context.Context, ns claims.NamespaceInfo, } res, err := s.queryUsers(ctx, sql, sqlQueryUsers, newListUser(sql, &query), limit) - if err == nil && query.UID != "" { res.RV, err = sql.GetResourceVersion(ctx, "user", "updated") } + return res, err } @@ -147,25 +146,24 @@ func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDa if err != nil { return res, err } + lastID = u.ID res.Users = append(res.Users, u) if len(res.Users) > limit { - res.ContinueID = lastID + res.Users = res.Users[0 : len(res.Users)-1] + res.Continue = lastID break } } } + return res, err } // ListTeamsBindings implements LegacyIdentityStore. func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error) { - if query.Limit < 1 { - query.Limit = 50 - } - - limit := int(query.Limit) - query.Limit += 1 // for continue + // for continue + query.Pagination.Limit += 1 query.OrgID = ns.OrgID if query.OrgID == 0 { return nil, fmt.Errorf("expected non zero orgID") @@ -214,9 +212,9 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa grouped[m.TeamUID] = []TeamMember{m} } - if len(grouped) >= limit { + if len(grouped) >= int(query.Pagination.Limit)-1 { atTeamLimit = true - res.ContinueID = lastID + res.Continue = lastID } } @@ -235,6 +233,62 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa return res, err } +// ListTeamMembers implements LegacyIdentityStore. +func (s *legacySQLStore) ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, 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 := newListTeamMembers(sql, &query) + q, err := sqltemplate.Execute(sqlQueryTeamMembers, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeams.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 := &ListTeamMembersResult{} + var lastID int64 + for rows.Next() { + m, err := scanMember(rows) + if err != nil { + return nil, err + } + + lastID = m.ID + res.Members = append(res.Members, m) + if len(res.Members) > int(query.Pagination.Limit)-1 { + res.Continue = lastID + res.Members = res.Members[0 : len(res.Members)-1] + break + } + } + + return res, err +} + +func scanMember(rows *sql.Rows) (TeamMember, error) { + m := TeamMember{} + 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/queries.go b/pkg/registry/apis/identity/legacy/queries.go index a1127b91dd3..5ee649b2196 100644 --- a/pkg/registry/apis/identity/legacy/queries.go +++ b/pkg/registry/apis/identity/legacy/queries.go @@ -30,6 +30,7 @@ var ( sqlQueryUsers = mustTemplate("query_users.sql") sqlQueryDisplay = mustTemplate("query_display.sql") sqlQueryTeamBindings = mustTemplate("query_team_bindings.sql") + sqlQueryTeamMembers = mustTemplate("query_team_members.sql") ) type sqlQueryListUsers struct { @@ -111,3 +112,25 @@ func newListTeamBindings(sql *legacysql.LegacyDatabaseHelper, q *ListTeamBinding Query: q, } } + +type sqlQueryListTeamMembers struct { + sqltemplate.SQLTemplate + Query *ListTeamMembersQuery + UserTable string + TeamTable string + TeamMemberTable string +} + +func (r sqlQueryListTeamMembers) Validate() error { + return nil // TODO +} + +func newListTeamMembers(sql *legacysql.LegacyDatabaseHelper, q *ListTeamMembersQuery) sqlQueryListTeamMembers { + return sqlQueryListTeamMembers{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + UserTable: sql.Table("user"), + TeamTable: sql.Table("team"), + TeamMemberTable: sql.Table("team_member"), + Query: q, + } +} diff --git a/pkg/registry/apis/identity/legacy/queries_test.go b/pkg/registry/apis/identity/legacy/queries_test.go index 833a124f9dc..8027ff7af80 100644 --- a/pkg/registry/apis/identity/legacy/queries_test.go +++ b/pkg/registry/apis/identity/legacy/queries_test.go @@ -4,6 +4,7 @@ import ( "testing" "text/template" + "github.com/grafana/grafana/pkg/registry/apis/identity/common" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" @@ -41,6 +42,12 @@ func TestIdentityQueries(t *testing.T) { return &v } + listTeamMembers := func(q *ListTeamMembersQuery) sqltemplate.SQLTemplate { + v := newListTeamMembers(nodb, q) + v.SQLTemplate = mocks.NewTestingSQLTemplate() + return &v + } + mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ RootDir: "testdata", Templates: map[*template.Template][]mocks.TemplateTestCase{ @@ -48,20 +55,23 @@ func TestIdentityQueries(t *testing.T) { { Name: "teams_uid", Data: listTeams(&ListTeamQuery{ - UID: "abc", + UID: "abc", + Pagination: common.Pagination{Limit: 1}, }), }, { Name: "teams_page_1", Data: listTeams(&ListTeamQuery{ - Limit: 5, + Pagination: common.Pagination{Limit: 5}, }), }, { Name: "teams_page_2", Data: listTeams(&ListTeamQuery{ - ContinueID: 1, - Limit: 2, + Pagination: common.Pagination{ + Limit: 1, + Continue: 2, + }, }), }, }, @@ -69,20 +79,23 @@ func TestIdentityQueries(t *testing.T) { { Name: "users_uid", Data: listUsers(&ListUserQuery{ - UID: "abc", + UID: "abc", + Pagination: common.Pagination{Limit: 1}, }), }, { Name: "users_page_1", Data: listUsers(&ListUserQuery{ - Limit: 5, + Pagination: common.Pagination{Limit: 5}, }), }, { Name: "users_page_2", Data: listUsers(&ListUserQuery{ - ContinueID: 1, - Limit: 2, + Pagination: common.Pagination{ + Limit: 1, + Continue: 2, + }, }), }, }, @@ -114,23 +127,44 @@ func TestIdentityQueries(t *testing.T) { { Name: "team_1_bindings", Data: listTeamBindings(&ListTeamBindingsQuery{ - OrgID: 1, - UID: "team-1", + OrgID: 1, + UID: "team-1", + Pagination: common.Pagination{Limit: 1}, }), }, { Name: "team_bindings_page_1", Data: listTeamBindings(&ListTeamBindingsQuery{ - OrgID: 1, - Limit: 5, + OrgID: 1, + Pagination: common.Pagination{Limit: 5}, }), }, { Name: "team_bindings_page_2", Data: listTeamBindings(&ListTeamBindingsQuery{ + OrgID: 1, + Pagination: common.Pagination{ + Limit: 1, + Continue: 2, + }, + }), + }, + }, + sqlQueryTeamMembers: { + { + Name: "team_1_members_page_1", + Data: listTeamMembers(&ListTeamMembersQuery{ + UID: "team-1", OrgID: 1, - Limit: 5, - ContinueID: 5, + Pagination: common.Pagination{Limit: 1}, + }), + }, + { + Name: "team_1_members_page_2", + Data: listTeamMembers(&ListTeamMembersQuery{ + UID: "team-1", + OrgID: 1, + Pagination: common.Pagination{Limit: 1, Continue: 2}, }), }, }, diff --git a/pkg/registry/apis/identity/legacy/query_team_bindings.sql b/pkg/registry/apis/identity/legacy/query_team_bindings.sql index 4176247a592..2a53b975ac7 100644 --- a/pkg/registry/apis/identity/legacy/query_team_bindings.sql +++ b/pkg/registry/apis/identity/legacy/query_team_bindings.sql @@ -8,11 +8,12 @@ WHERE t.uid IN( SELECT uid FROM {{ .Ident .TeamTable }} t - {{ if .Query.ContinueID }} - WHERE t.id >= {{ .Arg .Query.ContinueID }} + {{ if .Query.Pagination.Continue }} + WHERE t.id >= {{ .Arg .Query.Pagination.Continue }} {{ end }} - ORDER BY t.id ASC LIMIT {{ .Arg .Query.Limit }} + ORDER BY t.id ASC LIMIT {{ .Arg .Query.Pagination.Limit }} ) {{ end }} AND tm.org_id = {{ .Arg .Query.OrgID}} +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/query_team_members.sql b/pkg/registry/apis/identity/legacy/query_team_members.sql new file mode 100644 index 00000000000..ce898cd3d9a --- /dev/null +++ b/pkg/registry/apis/identity/legacy/query_team_members.sql @@ -0,0 +1,24 @@ +SELECT + tm.id as id, + t.uid as team_uid, + t.id as team_id, + u.uid as user_uid, + u.id as user_id, + u.name, + u.email, + u.login, + tm.external, + tm.created, + tm.updated, + tm.permission +FROM {{ .Ident .TeamMemberTable }} tm +INNER JOIN {{ .Ident .TeamTable }} t ON tm.team_id = t.id +INNER JOIN {{ .Ident .UserTable }} u ON tm.user_id = u.id +WHERE + t.uid = {{ .Arg .Query.UID }} +{{- if .Query.Pagination.Continue }} + AND tm.id >= {{ .Arg .Query.Pagination.Continue }} +{{- end }} + AND tm.org_id = {{ .Arg .Query.OrgID }} +ORDER BY t.id ASC +LIMIT {{ .Arg .Query.Pagination.Limit }}; diff --git a/pkg/registry/apis/identity/legacy/query_teams.sql b/pkg/registry/apis/identity/legacy/query_teams.sql index 27af9537b0d..3fef332044c 100644 --- a/pkg/registry/apis/identity/legacy/query_teams.sql +++ b/pkg/registry/apis/identity/legacy/query_teams.sql @@ -4,8 +4,8 @@ SELECT id, uid, name, email, created, updated {{ if .Query.UID }} AND uid = {{ .Arg .Query.UID }} {{ end }} -{{ if .Query.ContinueID }} - AND id > {{ .Arg .Query.ContinueID }} +{{ if .Query.Pagination.Continue }} + AND id >= {{ .Arg .Query.Pagination.Continue }} {{ end }} ORDER BY id asc - LIMIT {{ .Arg .Query.Limit }} \ No newline at end of file + LIMIT {{ .Arg .Query.Pagination.Limit }} diff --git a/pkg/registry/apis/identity/legacy/query_users.sql b/pkg/registry/apis/identity/legacy/query_users.sql index 4d7cf0a4693..c7863b0fba5 100644 --- a/pkg/registry/apis/identity/legacy/query_users.sql +++ b/pkg/registry/apis/identity/legacy/query_users.sql @@ -4,10 +4,10 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, WHERE o.org_id = {{ .Arg .Query.OrgID }} AND u.is_service_account = {{ .Arg .Query.IsServiceAccount }} {{ if .Query.UID }} - AND uid = {{ .Arg .Query.UID }} + AND u.uid = {{ .Arg .Query.UID }} {{ end }} -{{ if .Query.ContinueID }} - AND id > {{ .Arg .Query.ContinueID }} +{{ if .Query.Pagination.Continue }} + AND u.id >= {{ .Arg .Query.Pagination.Continue }} {{ end }} ORDER BY u.id asc - LIMIT {{ .Arg .Query.Limit }} \ No newline at end of file + LIMIT {{ .Arg .Query.Pagination.Limit }} diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_1_bindings.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_1_bindings.sql index 7aa32fa1492..f33d0ee83cf 100755 --- a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_1_bindings.sql +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_1_bindings.sql @@ -4,4 +4,5 @@ INNER JOIN `grafana`.`user` u ON tm.user_id = u.id WHERE t.uid = 'team-1' AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_1.sql index df66acd40f3..adf675a6dd6 100755 --- a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_1.sql +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_1.sql @@ -8,4 +8,5 @@ WHERE ORDER BY t.id ASC LIMIT 5 ) AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_2.sql index 7593469a0e2..403c86ed591 100755 --- a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_bindings-team_bindings_page_2.sql @@ -5,8 +5,9 @@ WHERE t.uid IN( SELECT uid FROM `grafana`.`team` t - WHERE t.id >= 5 - ORDER BY t.id ASC LIMIT 5 + WHERE t.id >= 2 + ORDER BY t.id ASC LIMIT 1 ) AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_members-team_1_members_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_members-team_1_members_page_1.sql new file mode 100755 index 00000000000..a9e8a770b39 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_members-team_1_members_page_1.sql @@ -0,0 +1,21 @@ +SELECT + tm.id as id, + t.uid as team_uid, + t.id as team_id, + u.uid as user_uid, + u.id as user_id, + u.name, + u.email, + u.login, + tm.external, + tm.created, + tm.updated, + tm.permission +FROM `grafana`.`team_member` tm +INNER JOIN `grafana`.`team` t ON tm.team_id = t.id +INNER JOIN `grafana`.`user` u ON tm.user_id = u.id +WHERE + t.uid = 'team-1' + AND tm.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_members-team_1_members_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_members-team_1_members_page_2.sql new file mode 100755 index 00000000000..2e77b9940a7 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_team_members-team_1_members_page_2.sql @@ -0,0 +1,22 @@ +SELECT + tm.id as id, + t.uid as team_uid, + t.id as team_id, + u.uid as user_uid, + u.id as user_id, + u.name, + u.email, + u.login, + tm.external, + tm.created, + tm.updated, + tm.permission +FROM `grafana`.`team_member` tm +INNER JOIN `grafana`.`team` t ON tm.team_id = t.id +INNER JOIN `grafana`.`user` u ON tm.user_id = u.id +WHERE + t.uid = 'team-1' + AND tm.id >= 2 + AND tm.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_page_2.sql index a85e1c63467..2e2b9b017f0 100755 --- a/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_page_2.sql @@ -1,6 +1,6 @@ SELECT id, uid, name, email, created, updated FROM `grafana`.`team` WHERE org_id = 0 - AND id > 1 + AND id >= 2 ORDER BY id asc - LIMIT 2 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_uid.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_uid.sql index 21914c31dad..9430f662f7e 100755 --- a/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_uid.sql +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_teams-teams_uid.sql @@ -3,4 +3,4 @@ SELECT id, uid, name, email, created, updated WHERE org_id = 0 AND uid = 'abc' ORDER BY id asc - LIMIT 0 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_page_2.sql index 55e01b09d1e..c32659b6c3d 100755 --- a/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_page_2.sql @@ -3,6 +3,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND u.is_service_account = FALSE - AND id > 1 + AND u.id >= 2 ORDER BY u.id asc - LIMIT 2 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_uid.sql b/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_uid.sql index e906ae54dfc..5944036d745 100755 --- a/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_uid.sql +++ b/pkg/registry/apis/identity/legacy/testdata/mysql--query_users-users_uid.sql @@ -3,6 +3,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND u.is_service_account = FALSE - AND uid = 'abc' + AND u.uid = 'abc' ORDER BY u.id asc - LIMIT 0 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_1_bindings.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_1_bindings.sql index a14f452d62e..9a81f4214eb 100755 --- a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_1_bindings.sql +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_1_bindings.sql @@ -4,4 +4,5 @@ INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE t.uid = 'team-1' AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_1.sql index 8f394864f80..99540dc991a 100755 --- a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_1.sql +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_1.sql @@ -8,4 +8,5 @@ WHERE ORDER BY t.id ASC LIMIT 5 ) AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_2.sql index d5af50ec1b6..7c03e74ca30 100755 --- a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_bindings-team_bindings_page_2.sql @@ -5,8 +5,9 @@ WHERE t.uid IN( SELECT uid FROM "grafana"."team" t - WHERE t.id >= 5 - ORDER BY t.id ASC LIMIT 5 + WHERE t.id >= 2 + ORDER BY t.id ASC LIMIT 1 ) AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_members-team_1_members_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_members-team_1_members_page_1.sql new file mode 100755 index 00000000000..1b3003058bd --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_members-team_1_members_page_1.sql @@ -0,0 +1,21 @@ +SELECT + tm.id as id, + t.uid as team_uid, + t.id as team_id, + u.uid as user_uid, + u.id as user_id, + u.name, + u.email, + u.login, + tm.external, + tm.created, + tm.updated, + tm.permission +FROM "grafana"."team_member" tm +INNER JOIN "grafana"."team" t ON tm.team_id = t.id +INNER JOIN "grafana"."user" u ON tm.user_id = u.id +WHERE + t.uid = 'team-1' + AND tm.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_members-team_1_members_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_members-team_1_members_page_2.sql new file mode 100755 index 00000000000..5844256f102 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_team_members-team_1_members_page_2.sql @@ -0,0 +1,22 @@ +SELECT + tm.id as id, + t.uid as team_uid, + t.id as team_id, + u.uid as user_uid, + u.id as user_id, + u.name, + u.email, + u.login, + tm.external, + tm.created, + tm.updated, + tm.permission +FROM "grafana"."team_member" tm +INNER JOIN "grafana"."team" t ON tm.team_id = t.id +INNER JOIN "grafana"."user" u ON tm.user_id = u.id +WHERE + t.uid = 'team-1' + AND tm.id >= 2 + AND tm.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_page_2.sql index 9c4f8ce1ee8..acfdf440e13 100755 --- a/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_page_2.sql @@ -1,6 +1,6 @@ SELECT id, uid, name, email, created, updated FROM "grafana"."team" WHERE org_id = 0 - AND id > 1 + AND id >= 2 ORDER BY id asc - LIMIT 2 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_uid.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_uid.sql index 325b490e83b..ab5b4829a51 100755 --- a/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_uid.sql +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_teams-teams_uid.sql @@ -3,4 +3,4 @@ SELECT id, uid, name, email, created, updated WHERE org_id = 0 AND uid = 'abc' ORDER BY id asc - LIMIT 0 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_page_2.sql index d3ef396b51e..ed97b2c25d2 100755 --- a/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_page_2.sql @@ -3,6 +3,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND u.is_service_account = FALSE - AND id > 1 + AND u.id >= 2 ORDER BY u.id asc - LIMIT 2 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_uid.sql b/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_uid.sql index 985c51837c7..bab837475af 100755 --- a/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_uid.sql +++ b/pkg/registry/apis/identity/legacy/testdata/postgres--query_users-users_uid.sql @@ -3,6 +3,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND u.is_service_account = FALSE - AND uid = 'abc' + AND u.uid = 'abc' ORDER BY u.id asc - LIMIT 0 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_1_bindings.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_1_bindings.sql index a14f452d62e..9a81f4214eb 100755 --- a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_1_bindings.sql +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_1_bindings.sql @@ -4,4 +4,5 @@ INNER JOIN "grafana"."user" u ON tm.user_id = u.id WHERE t.uid = 'team-1' AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_1.sql index 8f394864f80..99540dc991a 100755 --- a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_1.sql +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_1.sql @@ -8,4 +8,5 @@ WHERE ORDER BY t.id ASC LIMIT 5 ) AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_2.sql index d5af50ec1b6..7c03e74ca30 100755 --- a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_bindings-team_bindings_page_2.sql @@ -5,8 +5,9 @@ WHERE t.uid IN( SELECT uid FROM "grafana"."team" t - WHERE t.id >= 5 - ORDER BY t.id ASC LIMIT 5 + WHERE t.id >= 2 + ORDER BY t.id ASC LIMIT 1 ) AND tm.org_id = 1 +AND NOT tm.external ORDER BY t.id ASC; diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_members-team_1_members_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_members-team_1_members_page_1.sql new file mode 100755 index 00000000000..1b3003058bd --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_members-team_1_members_page_1.sql @@ -0,0 +1,21 @@ +SELECT + tm.id as id, + t.uid as team_uid, + t.id as team_id, + u.uid as user_uid, + u.id as user_id, + u.name, + u.email, + u.login, + tm.external, + tm.created, + tm.updated, + tm.permission +FROM "grafana"."team_member" tm +INNER JOIN "grafana"."team" t ON tm.team_id = t.id +INNER JOIN "grafana"."user" u ON tm.user_id = u.id +WHERE + t.uid = 'team-1' + AND tm.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_members-team_1_members_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_members-team_1_members_page_2.sql new file mode 100755 index 00000000000..5844256f102 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_team_members-team_1_members_page_2.sql @@ -0,0 +1,22 @@ +SELECT + tm.id as id, + t.uid as team_uid, + t.id as team_id, + u.uid as user_uid, + u.id as user_id, + u.name, + u.email, + u.login, + tm.external, + tm.created, + tm.updated, + tm.permission +FROM "grafana"."team_member" tm +INNER JOIN "grafana"."team" t ON tm.team_id = t.id +INNER JOIN "grafana"."user" u ON tm.user_id = u.id +WHERE + t.uid = 'team-1' + AND tm.id >= 2 + AND tm.org_id = 1 +ORDER BY t.id ASC +LIMIT 1; diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_page_2.sql index 9c4f8ce1ee8..acfdf440e13 100755 --- a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_page_2.sql @@ -1,6 +1,6 @@ SELECT id, uid, name, email, created, updated FROM "grafana"."team" WHERE org_id = 0 - AND id > 1 + AND id >= 2 ORDER BY id asc - LIMIT 2 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_uid.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_uid.sql index 325b490e83b..ab5b4829a51 100755 --- a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_uid.sql +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_teams-teams_uid.sql @@ -3,4 +3,4 @@ SELECT id, uid, name, email, created, updated WHERE org_id = 0 AND uid = 'abc' ORDER BY id asc - LIMIT 0 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_page_2.sql index d3ef396b51e..ed97b2c25d2 100755 --- a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_page_2.sql +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_page_2.sql @@ -3,6 +3,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND u.is_service_account = FALSE - AND id > 1 + AND u.id >= 2 ORDER BY u.id asc - LIMIT 2 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_uid.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_uid.sql index 985c51837c7..bab837475af 100755 --- a/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_uid.sql +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite--query_users-users_uid.sql @@ -3,6 +3,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND u.is_service_account = FALSE - AND uid = 'abc' + AND u.uid = 'abc' ORDER BY u.id asc - LIMIT 0 + LIMIT 1 diff --git a/pkg/registry/apis/identity/legacy/types.go b/pkg/registry/apis/identity/legacy/types.go index 08b42434c17..3d63f81cdac 100644 --- a/pkg/registry/apis/identity/legacy/types.go +++ b/pkg/registry/apis/identity/legacy/types.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/identity/common" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" ) @@ -13,15 +14,15 @@ import ( type ListUserQuery struct { OrgID int64 UID string - ContinueID int64 // ContinueID - Limit int64 IsServiceAccount bool + + Pagination common.Pagination } type ListUserResult struct { - Users []user.User - ContinueID int64 - RV int64 + Users []user.User + Continue int64 + RV int64 } type GetUserDisplayQuery struct { @@ -31,23 +32,28 @@ type GetUserDisplayQuery struct { } type ListTeamQuery struct { - OrgID int64 - UID string - ContinueID int64 // ContinueID - Limit int64 + OrgID int64 + UID string + + Pagination common.Pagination } type ListTeamResult struct { - Teams []team.Team - ContinueID int64 - RV int64 + Teams []team.Team + Continue int64 + RV int64 } type TeamMember struct { ID int64 TeamID int64 TeamUID string + UserID int64 UserUID string + Name string + Email string + Username string + External bool Updated time.Time Created time.Time Permission team.PermissionType @@ -66,23 +72,35 @@ type ListTeamBindingsQuery struct { // UID is team uid to list bindings for. If not set store should list bindings for all teams UID string OrgID int64 - ContinueID int64 // ContinueID - Limit int64 + Pagination common.Pagination } type ListTeamBindingsResult struct { - Bindings []TeamBinding - ContinueID int64 - RV int64 + Bindings []TeamBinding + Continue int64 + RV int64 +} + +type ListTeamMembersQuery struct { + UID string + OrgID int64 + Pagination common.Pagination +} + +type ListTeamMembersResult struct { + Continue int64 + Members []TeamMember } // In every case, RBAC should be applied before calling, or before returning results to the requester type LegacyIdentityStore interface { - ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error) GetDisplay(ctx context.Context, ns claims.NamespaceInfo, query GetUserDisplayQuery) (*ListUserResult, error) + ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, 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) } diff --git a/pkg/registry/apis/identity/register.go b/pkg/registry/apis/identity/register.go index 22079582c7b..2bc2b5b0a20 100644 --- a/pkg/registry/apis/identity/register.go +++ b/pkg/registry/apis/identity/register.go @@ -82,6 +82,7 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo( teamResource := identityv0.TeamResourceInfo storage[teamResource.StoragePath()] = team.NewLegacyStore(b.Store) + storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.Store) teamBindingResource := identityv0.TeamBindingResourceInfo storage[teamBindingResource.StoragePath()] = team.NewLegacyBindingStore(b.Store) diff --git a/pkg/registry/apis/identity/serviceaccount/store.go b/pkg/registry/apis/identity/serviceaccount/store.go index 2d963c17248..2f6a87540a7 100644 --- a/pkg/registry/apis/identity/serviceaccount/store.go +++ b/pkg/registry/apis/identity/serviceaccount/store.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" 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" "github.com/grafana/grafana/pkg/services/user" @@ -64,14 +65,8 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } query := legacy.ListUserQuery{ OrgID: ns.OrgID, - Limit: options.Limit, IsServiceAccount: true, - } - if options.Continue != "" { - query.ContinueID, err = strconv.ParseInt(options.Continue, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid continue token") - } + Pagination: common.PaginationFromListOptions(options), } found, err := s.store.ListUsers(ctx, ns, query) @@ -83,12 +78,10 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt for _, item := range found.Users { list.Items = append(list.Items, *toSAItem(&item, ns.Value)) } - if found.ContinueID > 0 { - list.ListMeta.Continue = strconv.FormatInt(found.ContinueID, 10) - } - if found.RV > 0 { - list.ListMeta.ResourceVersion = strconv.FormatInt(found.RV, 10) - } + + list.ListMeta.Continue = common.OptionalFormatInt(found.Continue) + list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV) + return list, err } @@ -123,8 +116,8 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO } query := legacy.ListUserQuery{ OrgID: ns.OrgID, - Limit: 1, IsServiceAccount: true, + Pagination: common.Pagination{Limit: 1}, } found, err := s.store.ListUsers(ctx, ns, query) diff --git a/pkg/registry/apis/identity/team/rest_members.go b/pkg/registry/apis/identity/team/rest_members.go new file mode 100644 index 00000000000..2279c489235 --- /dev/null +++ b/pkg/registry/apis/identity/team/rest_members.go @@ -0,0 +1,112 @@ +package team + +import ( + "context" + "net/http" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/api/dtos" + 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" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + _ rest.Storage = (*LegacyTeamMemberREST)(nil) + _ rest.Scoper = (*LegacyTeamMemberREST)(nil) + _ rest.StorageMetadata = (*LegacyTeamMemberREST)(nil) + _ rest.Connecter = (*LegacyTeamMemberREST)(nil) +) + +func NewLegacyTeamMemberREST(store legacy.LegacyIdentityStore) *LegacyTeamMemberREST { + return &LegacyTeamMemberREST{store} +} + +type LegacyTeamMemberREST struct { + store legacy.LegacyIdentityStore +} + +// New implements rest.Storage. +func (s *LegacyTeamMemberREST) New() runtime.Object { + return &identityv0.TeamMemberList{} +} + +// Destroy implements rest.Storage. +func (s *LegacyTeamMemberREST) Destroy() {} + +// NamespaceScoped implements rest.Scoper. +func (s *LegacyTeamMemberREST) NamespaceScoped() bool { + return true +} + +// ProducesMIMETypes implements rest.StorageMetadata. +func (s *LegacyTeamMemberREST) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +// ProducesObject implements rest.StorageMetadata. +func (s *LegacyTeamMemberREST) ProducesObject(verb string) interface{} { + return s.New() +} + +// Connect implements rest.Connecter. +func (s *LegacyTeamMemberREST) 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) { + _ = common.PaginationFromListQuery(r.URL.Query()) + + res, err := s.store.ListTeamMembers(ctx, ns, legacy.ListTeamMembersQuery{ + UID: name, + Pagination: common.PaginationFromListQuery(r.URL.Query()), + }) + if err != nil { + responder.Error(err) + return + } + + list := &identityv0.TeamMemberList{Items: make([]identityv0.TeamMember, 0, len(res.Members))} + + for _, m := range res.Members { + list.Items = append(list.Items, mapToTeamMember(m)) + } + + list.ListMeta.Continue = common.OptionalFormatInt(res.Continue) + + responder.Object(http.StatusOK, list) + }), nil +} + +// NewConnectOptions implements rest.Connecter. +func (s *LegacyTeamMemberREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +// ConnectMethods implements rest.Connecter. +func (s *LegacyTeamMemberREST) ConnectMethods() []string { + return []string{http.MethodGet} +} + +var cfg = &setting.Cfg{} + +func mapToTeamMember(m legacy.TeamMember) identityv0.TeamMember { + return identityv0.TeamMember{ + IdentityDisplay: identityv0.IdentityDisplay{ + IdentityType: claims.TypeUser, + UID: m.UserUID, + Display: m.Name, + AvatarURL: dtos.GetGravatarUrlWithDefault(cfg, m.Email, m.Name), + InternalID: m.UserID, + }, + External: m.External, + Permission: mapPermisson(m.Permission), + } +} diff --git a/pkg/registry/apis/identity/team/store.go b/pkg/registry/apis/identity/team/store.go index 7bfde79ad35..06ef7769936 100644 --- a/pkg/registry/apis/identity/team/store.go +++ b/pkg/registry/apis/identity/team/store.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/utils" 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" "github.com/grafana/grafana/pkg/services/team" @@ -59,10 +60,6 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) doList(ctx context.Context, ns claims.NamespaceInfo, query legacy.ListTeamQuery) (*identityv0.TeamList, error) { - if query.Limit < 1 { - query.Limit = 100 - } - rsp, err := s.store.ListTeams(ctx, ns, query) if err != nil { return nil, err @@ -96,9 +93,10 @@ func (s *LegacyStore) doList(ctx context.Context, ns claims.NamespaceInfo, query }) list.Items = append(list.Items, item) } - if rsp.ContinueID > 0 { - list.ListMeta.Continue = strconv.FormatInt(rsp.ContinueID, 10) - } + + list.ListMeta.Continue = common.OptionalFormatInt(rsp.Continue) + list.ListMeta.ResourceVersion = common.OptionalFormatInt(rsp.RV) + return list, nil } @@ -107,17 +105,11 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt if err != nil { return nil, err } - query := legacy.ListTeamQuery{ - OrgID: ns.OrgID, - Limit: options.Limit, - } - if options.Continue != "" { - query.ContinueID, err = strconv.ParseInt(options.Continue, 10, 64) - if err != nil { - return nil, err - } - } - return s.doList(ctx, ns, query) + + return s.doList(ctx, ns, legacy.ListTeamQuery{ + OrgID: ns.OrgID, + Pagination: common.PaginationFromListOptions(options), + }) } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { @@ -126,9 +118,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO return nil, err } rsp, err := s.doList(ctx, ns, legacy.ListTeamQuery{ - OrgID: ns.OrgID, - Limit: 1, - UID: name, + OrgID: ns.OrgID, + UID: name, + Pagination: common.Pagination{Limit: 1}, }) if err != nil { return nil, err diff --git a/pkg/registry/apis/identity/team/store_binding.go b/pkg/registry/apis/identity/team/store_binding.go index 480cbe90c8e..6c1abc8593b 100644 --- a/pkg/registry/apis/identity/team/store_binding.go +++ b/pkg/registry/apis/identity/team/store_binding.go @@ -72,8 +72,8 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta } res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{ - UID: name, - Limit: 1, + UID: name, + Pagination: common.Pagination{Limit: 1}, }) if err != nil { return nil, err @@ -95,14 +95,8 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion. return nil, err } - continueID, err := common.GetContinueID(options) - if err != nil { - return nil, err - } - res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{ - ContinueID: continueID, - Limit: options.Limit, + Pagination: common.PaginationFromListOptions(options), }) if err != nil { return nil, err @@ -116,7 +110,7 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion. list.Items = append(list.Items, mapToBindingObject(ns, b)) } - list.ListMeta.Continue = common.OptionalFormatInt(res.ContinueID) + list.ListMeta.Continue = common.OptionalFormatInt(res.Continue) list.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV) return &list, nil diff --git a/pkg/registry/apis/identity/user/display_store.go b/pkg/registry/apis/identity/user/display_store.go index 6d399968520..406e4c2e0c0 100644 --- a/pkg/registry/apis/identity/user/display_store.go +++ b/pkg/registry/apis/identity/user/display_store.go @@ -46,7 +46,7 @@ func (r *LegacyDisplayStore) NamespaceScoped() bool { func (r *LegacyDisplayStore) GetSingularName() string { // not actually used anywhere, but required by SingularNameProvider - return "IdentityDisplay" + return "identitydisplay" } func (r *LegacyDisplayStore) ProducesMIMETypes(verb string) []string { diff --git a/pkg/registry/apis/identity/user/store.go b/pkg/registry/apis/identity/user/store.go index 80066c20efc..9130638717f 100644 --- a/pkg/registry/apis/identity/user/store.go +++ b/pkg/registry/apis/identity/user/store.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" 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" "github.com/grafana/grafana/pkg/services/user" @@ -62,19 +63,12 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt if err != nil { return nil, err } - query := legacy.ListUserQuery{ - OrgID: ns.OrgID, - Limit: options.Limit, - IsServiceAccount: false, - } - if options.Continue != "" { - query.ContinueID, err = strconv.ParseInt(options.Continue, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid continue token") - } - } - found, err := s.store.ListUsers(ctx, ns, query) + found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ + OrgID: ns.OrgID, + IsServiceAccount: false, + Pagination: common.PaginationFromListOptions(options), + }) if err != nil { return nil, err } @@ -83,12 +77,10 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt for _, item := range found.Users { list.Items = append(list.Items, *toUserItem(&item, ns.Value)) } - if found.ContinueID > 0 { - list.ListMeta.Continue = strconv.FormatInt(found.ContinueID, 10) - } - if found.RV > 0 { - list.ListMeta.ResourceVersion = strconv.FormatInt(found.RV, 10) - } + + list.ListMeta.Continue = common.OptionalFormatInt(found.Continue) + list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV) + return list, err } @@ -99,8 +91,8 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO } query := legacy.ListUserQuery{ OrgID: ns.OrgID, - Limit: 1, IsServiceAccount: false, + Pagination: common.Pagination{Limit: 1}, } found, err := s.store.ListUsers(ctx, ns, query)