From a0cd89860e018b1fa02b53f7c50d7df415808f76 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 15 Aug 2024 14:38:43 +0300 Subject: [PATCH] Identity: Add endpoint to get display info for an identifier (#91828) --- pkg/api/admin_users.go | 2 +- pkg/api/http_server.go | 4 +- pkg/api/user.go | 2 +- .../apis/identity/v0alpha1/register.go | 2 +- .../apis/identity/v0alpha1/types.go | 26 ++- .../v0alpha1/zz_generated.deepcopy.go | 25 ++- .../identity/v0alpha1/zz_generated.openapi.go | 79 +++++-- ...enerated.openapi_violation_exceptions.list | 2 +- .../apis/dashboard/legacy/sql_dashboards.go | 43 +--- pkg/registry/apis/identity/display.go | 191 ++++++++++++++++ .../apis/identity/legacy/legacy_sql.go | 186 ++++++++++++++++ pkg/registry/apis/identity/legacy/queries.go | 58 +++++ .../apis/identity/legacy/queries_test.go | 207 ++++++++++++++++++ .../apis/identity/legacy/query_display.sql | 13 ++ .../apis/identity/legacy/query_teams.sql | 11 + .../apis/identity/legacy/query_users.sql | 13 ++ .../legacy/testdata/mysql__display_ids.sql | 8 + .../testdata/mysql__display_ids_uids.sql | 9 + .../legacy/testdata/mysql__display_uids.sql | 8 + .../legacy/testdata/mysql__teams_page_1.sql | 5 + .../legacy/testdata/mysql__teams_page_2.sql | 6 + .../legacy/testdata/mysql__teams_uid.sql | 6 + .../legacy/testdata/mysql__users_page_1.sql | 7 + .../legacy/testdata/mysql__users_page_2.sql | 8 + .../legacy/testdata/mysql__users_uid.sql | 8 + .../legacy/testdata/postgres__display_ids.sql | 8 + .../testdata/postgres__display_ids_uids.sql | 9 + .../testdata/postgres__display_uids.sql | 8 + .../testdata/postgres__teams_page_1.sql | 5 + .../testdata/postgres__teams_page_2.sql | 6 + .../legacy/testdata/postgres__teams_uid.sql | 6 + .../testdata/postgres__users_page_1.sql | 7 + .../testdata/postgres__users_page_2.sql | 8 + .../legacy/testdata/postgres__users_uid.sql | 8 + .../legacy/testdata/sqlite__display_ids.sql | 8 + .../testdata/sqlite__display_ids_uids.sql | 9 + .../legacy/testdata/sqlite__display_uids.sql | 8 + .../legacy/testdata/sqlite__teams_page_1.sql | 5 + .../legacy/testdata/sqlite__teams_page_2.sql | 6 + .../legacy/testdata/sqlite__teams_uid.sql | 6 + .../legacy/testdata/sqlite__users_page_1.sql | 7 + .../legacy/testdata/sqlite__users_page_2.sql | 8 + .../legacy/testdata/sqlite__users_uid.sql | 8 + pkg/registry/apis/identity/legacy/types.go | 50 +++++ pkg/registry/apis/identity/legacy_sa.go | 38 ++-- pkg/registry/apis/identity/legacy_teams.go | 75 +++++-- .../apis/identity/legacy_user_teams.go | 87 ++++++++ pkg/registry/apis/identity/legacy_users.go | 43 ++-- pkg/registry/apis/identity/register.go | 39 ++-- .../apiserver/auth/authorizer/org_id.go | 5 + pkg/services/apiserver/builder/helper.go | 6 + pkg/services/team/model.go | 7 - pkg/services/team/team.go | 1 - pkg/services/team/teamimpl/store.go | 16 -- pkg/services/team/teamimpl/team.go | 8 - pkg/services/team/teamtest/team.go | 4 - pkg/services/user/model.go | 7 - pkg/services/user/user.go | 1 - pkg/services/user/userimpl/store.go | 35 --- pkg/services/user/userimpl/user.go | 9 - pkg/services/user/userimpl/user_test.go | 4 - pkg/services/user/usertest/fake.go | 4 - pkg/services/user/usertest/mock.go | 30 --- pkg/storage/legacysql/README.md | 8 + pkg/storage/legacysql/rv.go | 51 +++++ pkg/tests/apis/helper.go | 80 +++++-- pkg/tests/apis/identity/identity_test.go | 150 +++++++++++++ 67 files changed, 1535 insertions(+), 282 deletions(-) create mode 100644 pkg/registry/apis/identity/display.go create mode 100644 pkg/registry/apis/identity/legacy/legacy_sql.go create mode 100644 pkg/registry/apis/identity/legacy/queries.go create mode 100644 pkg/registry/apis/identity/legacy/queries_test.go create mode 100644 pkg/registry/apis/identity/legacy/query_display.sql create mode 100644 pkg/registry/apis/identity/legacy/query_teams.sql create mode 100644 pkg/registry/apis/identity/legacy/query_users.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__display_ids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__display_ids_uids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__display_uids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__teams_uid.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__users_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__users_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/mysql__users_uid.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__display_ids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__display_ids_uids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__display_uids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__teams_uid.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__users_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__users_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/postgres__users_uid.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids_uids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__display_uids.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__teams_uid.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_1.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_2.sql create mode 100755 pkg/registry/apis/identity/legacy/testdata/sqlite__users_uid.sql create mode 100644 pkg/registry/apis/identity/legacy/types.go create mode 100644 pkg/registry/apis/identity/legacy_user_teams.go create mode 100644 pkg/storage/legacysql/README.md create mode 100644 pkg/storage/legacysql/rv.go create mode 100644 pkg/tests/apis/identity/identity_test.go diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 83b1ae516af..456484d54f8 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -229,7 +229,7 @@ func (hs *HTTPServer) AdminDeleteUser(c *contextmodel.ReqContext) response.Respo return nil }) g.Go(func() error { - if err := hs.teamService.RemoveUsersMemberships(ctx, cmd.UserID); err != nil { + if err := hs.TeamService.RemoveUsersMemberships(ctx, cmd.UserID); err != nil { return err } return nil diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 75463d6c0d5..9f5ac6b79fd 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -202,7 +202,7 @@ type HTTPServer struct { tempUserService tempUser.Service loginAttemptService loginAttempt.Service orgService org.Service - teamService team.Service + TeamService team.Service accesscontrolService accesscontrol.Service annotationsRepo annotations.Repository tagService tag.Service @@ -352,7 +352,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi tempUserService: tempUserService, loginAttemptService: loginAttemptService, orgService: orgService, - teamService: teamService, + TeamService: teamService, navTreeService: navTreeService, accesscontrolService: accesscontrolService, annotationsRepo: annotationRepo, diff --git a/pkg/api/user.go b/pkg/api/user.go index 3f66caeef38..eb3b2c265ca 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -401,7 +401,7 @@ func (hs *HTTPServer) GetUserTeams(c *contextmodel.ReqContext) response.Response func (hs *HTTPServer) getUserTeamList(c *contextmodel.ReqContext, orgID int64, userID int64) response.Response { query := team.GetTeamsByUserQuery{OrgID: orgID, UserID: userID, SignedInUser: c.SignedInUser} - queryResult, err := hs.teamService.GetTeamsByUser(c.Req.Context(), &query) + queryResult, err := hs.TeamService.GetTeamsByUser(c.Req.Context(), &query) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to get user teams", err) } diff --git a/pkg/apimachinery/apis/identity/v0alpha1/register.go b/pkg/apimachinery/apis/identity/v0alpha1/register.go index 18ad5cc0677..3c1ddaeaa1f 100644 --- a/pkg/apimachinery/apis/identity/v0alpha1/register.go +++ b/pkg/apimachinery/apis/identity/v0alpha1/register.go @@ -120,7 +120,7 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) error { &ServiceAccountList{}, &Team{}, &TeamList{}, - &IdentityDisplayList{}, + &IdentityDisplayResults{}, ) // metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apimachinery/apis/identity/v0alpha1/types.go b/pkg/apimachinery/apis/identity/v0alpha1/types.go index 75fd33e9b73..05c286d79ac 100644 --- a/pkg/apimachinery/apis/identity/v0alpha1/types.go +++ b/pkg/apimachinery/apis/identity/v0alpha1/types.go @@ -1,6 +1,7 @@ package v0alpha1 import ( + "github.com/grafana/authlib/claims" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -73,19 +74,28 @@ type ServiceAccountList struct { } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type IdentityDisplayList struct { +type IdentityDisplayResults struct { metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []IdentityDisplay `json:"items,omitempty"` + // Request keys used to lookup the display value + // +listType=set + Keys []string `json:"keys"` + + // Matching items (the caller may need to remap from keys to results) + // +listType=atomic + Display []IdentityDisplay `json:"display"` + + // Input keys that were not useable + // +listType=set + InvalidKeys []string `json:"invalidKeys,omitempty"` } type IdentityDisplay struct { - IdentityType string `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"` + 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"` // Legacy internal ID -- usage of this value should be phased out - LegacyID int64 `json:"legacyId,omitempty"` + InternalID int64 `json:"internalId,omitempty"` } diff --git a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.deepcopy.go b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.deepcopy.go index 70fbea9caa6..fba461df2e9 100644 --- a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.deepcopy.go @@ -28,30 +28,39 @@ func (in *IdentityDisplay) DeepCopy() *IdentityDisplay { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IdentityDisplayList) DeepCopyInto(out *IdentityDisplayList) { +func (in *IdentityDisplayResults) DeepCopyInto(out *IdentityDisplayResults) { *out = *in out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items + if in.Keys != nil { + in, out := &in.Keys, &out.Keys + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Display != nil { + in, out := &in.Display, &out.Display *out = make([]IdentityDisplay, len(*in)) copy(*out, *in) } + if in.InvalidKeys != nil { + in, out := &in.InvalidKeys, &out.InvalidKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentityDisplayList. -func (in *IdentityDisplayList) DeepCopy() *IdentityDisplayList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentityDisplayResults. +func (in *IdentityDisplayResults) DeepCopy() *IdentityDisplayResults { if in == nil { return nil } - out := new(IdentityDisplayList) + out := new(IdentityDisplayResults) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IdentityDisplayList) DeepCopyObject() runtime.Object { +func (in *IdentityDisplayResults) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } diff --git a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi.go b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi.go index 5027aa47724..a5e4db9ca2b 100644 --- a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi.go +++ b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi.go @@ -14,17 +14,17 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplay": schema_apimachinery_apis_identity_v0alpha1_IdentityDisplay(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplayList": schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayList(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccount": schema_apimachinery_apis_identity_v0alpha1_ServiceAccount(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountList": schema_apimachinery_apis_identity_v0alpha1_ServiceAccountList(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountSpec": schema_apimachinery_apis_identity_v0alpha1_ServiceAccountSpec(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.Team": schema_apimachinery_apis_identity_v0alpha1_Team(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamList": schema_apimachinery_apis_identity_v0alpha1_TeamList(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamSpec": schema_apimachinery_apis_identity_v0alpha1_TeamSpec(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.User": schema_apimachinery_apis_identity_v0alpha1_User(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserList": schema_apimachinery_apis_identity_v0alpha1_UserList(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserSpec": schema_apimachinery_apis_identity_v0alpha1_UserSpec(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplay": schema_apimachinery_apis_identity_v0alpha1_IdentityDisplay(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplayResults": schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayResults(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccount": schema_apimachinery_apis_identity_v0alpha1_ServiceAccount(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountList": schema_apimachinery_apis_identity_v0alpha1_ServiceAccountList(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.ServiceAccountSpec": schema_apimachinery_apis_identity_v0alpha1_ServiceAccountSpec(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.Team": schema_apimachinery_apis_identity_v0alpha1_Team(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamList": schema_apimachinery_apis_identity_v0alpha1_TeamList(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.TeamSpec": schema_apimachinery_apis_identity_v0alpha1_TeamSpec(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.User": schema_apimachinery_apis_identity_v0alpha1_User(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserList": schema_apimachinery_apis_identity_v0alpha1_UserList(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.UserSpec": schema_apimachinery_apis_identity_v0alpha1_UserSpec(ref), } } @@ -63,7 +63,7 @@ func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplay(ref common.Refer Format: "", }, }, - "legacyId": { + "internalId": { SchemaProps: spec.SchemaProps{ Description: "Legacy internal ID -- usage of this value should be phased out", Type: []string{"integer"}, @@ -77,7 +77,7 @@ func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplay(ref common.Refer } } -func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayResults(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -97,15 +97,35 @@ func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayList(ref common.R Format: "", }, }, - "metadata": { + "keys": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Description: "Request keys used to lookup the display value", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, }, }, - "items": { + "display": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, + Description: "Matching items (the caller may need to remap from keys to results)", + Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -116,11 +136,32 @@ func schema_apimachinery_apis_identity_v0alpha1_IdentityDisplayList(ref common.R }, }, }, + "invalidKeys": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Input keys that were not useable", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, + Required: []string{"keys", "display"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplay", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1.IdentityDisplay"}, } } diff --git a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list index bab555666f5..93ccd0928ad 100644 --- a/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apimachinery/apis/identity/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,3 +1,3 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1,IdentityDisplay,IdentityType -API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1,IdentityDisplay,LegacyID +API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1,IdentityDisplay,InternalID API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1,TeamSpec,Title diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 8a17e906f3f..93e6bbd401a 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -19,8 +19,7 @@ import ( gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/services/sqlstore/session" + "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -47,13 +46,12 @@ type dashboardRow struct { } type dashboardSqlAccess struct { - sql db.DB + sql legacysql.NamespacedDBProvider dialect sqltemplate.Dialect - sess *session.SessionDB namespacer request.NamespaceMapper dashStore dashboards.Store provisioning provisioning.ProvisioningService - currentRV func(ctx context.Context) (int64, error) + currentRV legacysql.ResourceVersionLookup // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent @@ -72,38 +70,15 @@ func NewDashboardAccess(sql db.DB, fmt.Printf("ERROR: NO DIALECT") } - sess := sql.GetSqlxSession() - currentRV := func(ctx context.Context) (int64, error) { - t := time.Now() - max := "" - err := sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard") - if err == nil && max != "" { - t, _ = time.Parse(time.DateTime, max) // ignore null errors - } - return t.UnixMilli(), nil - } - if sql.GetDBType() == migrator.Postgres { - currentRV = func(ctx context.Context) (int64, error) { - max := time.Now() - _ = sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard") - return max.UnixMilli(), nil - } - } else if sql.GetDBType() == migrator.MySQL { - currentRV = func(ctx context.Context) (int64, error) { - max := time.Now().UnixMilli() - _ = sess.Get(ctx, &max, "SELECT UNIX_TIMESTAMP(MAX(updated)) FROM dashboard;") - return max, nil - } - } + nssql := func(ctx context.Context) (db.DB, error) { return sql, nil } return &dashboardSqlAccess{ - sql: sql, - sess: sess, + sql: nssql, dialect: dialect, namespacer: namespacer, dashStore: dashStore, provisioning: provisioning, - currentRV: currentRV, + currentRV: legacysql.GetResourceVersionLookup(nssql, "dashboard", "updated"), } } @@ -134,7 +109,11 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery) // q = sqltemplate.RemoveEmptyLines(rawQuery) // fmt.Printf(">>%s [%+v]", q, req.GetArgs()) - rows, err := a.sess.Query(ctx, q, req.GetArgs()...) + db, err := a.sql(ctx) + if err != nil { + return nil, err + } + rows, err := db.GetSqlxSession().Query(ctx, q, req.GetArgs()...) if err != nil { if rows != nil { _ = rows.Close() diff --git a/pkg/registry/apis/identity/display.go b/pkg/registry/apis/identity/display.go new file mode 100644 index 00000000000..16527fbdaa2 --- /dev/null +++ b/pkg/registry/apis/identity/display.go @@ -0,0 +1,191 @@ +package identity + +import ( + "context" + "net/http" + "strconv" + "strings" + + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/api/dtos" + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/identity/legacy" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/setting" + errorsK8s "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" +) + +type displayREST struct { + store legacy.LegacyIdentityStore +} + +var ( + _ rest.Storage = (*displayREST)(nil) + _ rest.SingularNameProvider = (*displayREST)(nil) + _ rest.Connecter = (*displayREST)(nil) + _ rest.Scoper = (*displayREST)(nil) + _ rest.StorageMetadata = (*displayREST)(nil) +) + +func newDisplayREST(store legacy.LegacyIdentityStore) *displayREST { + return &displayREST{store} +} + +func (r *displayREST) New() runtime.Object { + return &identity.IdentityDisplayResults{} +} + +func (r *displayREST) Destroy() {} + +func (r *displayREST) NamespaceScoped() bool { + return true +} + +func (r *displayREST) GetSingularName() string { + return "IdentityDisplay" // not actually used anywhere, but required by SingularNameProvider +} + +func (r *displayREST) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (r *displayREST) ProducesObject(verb string) any { + return &identity.IdentityDisplayResults{} +} + +func (r *displayREST) ConnectMethods() []string { + return []string{"GET"} +} + +func (r *displayREST) 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 *displayREST) 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" { + return nil, errorsK8s.NewNotFound(schema.GroupResource{}, name) + } + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + keys := parseKeys(req.URL.Query()["key"]) + users, err := r.store.GetDisplay(ctx, ns, legacy.GetUserDisplayQuery{ + OrgID: ns.OrgID, + UIDs: keys.uids, + IDs: keys.ids, + }) + if err != nil { + responder.Error(err) + return + } + + rsp := &identity.IdentityDisplayResults{ + Keys: keys.keys, + InvalidKeys: keys.invalid, + Display: make([]identity.IdentityDisplay, 0, len(users.Users)+len(keys.disp)+1), + } + for _, user := range users.Users { + disp := identity.IdentityDisplay{ + IdentityType: claims.TypeUser, + Display: user.NameOrFallback(), + UID: user.UID, + } + if user.IsServiceAccount { + disp.IdentityType = claims.TypeServiceAccount + } + disp.AvatarURL = dtos.GetGravatarUrlWithDefault(fakeCfgForGravatar, user.Email, disp.Display) + rsp.Display = append(rsp.Display, disp) + } + + // Append the constants here + if len(keys.disp) > 0 { + rsp.Display = append(rsp.Display, keys.disp...) + } + responder.Object(200, rsp) + }), nil +} + +type dispKeys struct { + keys []string + uids []string + ids []int64 + invalid []string + + // For terminal keys, this is a constant + disp []identity.IdentityDisplay +} + +func parseKeys(req []string) dispKeys { + keys := dispKeys{ + uids: make([]string, 0, len(req)), + ids: make([]int64, 0, len(req)), + keys: req, + } + for _, key := range req { + idx := strings.Index(key, ":") + if idx > 0 { + t, err := claims.ParseType(key[0:idx]) + if err != nil { + keys.invalid = append(keys.invalid, key) + continue + } + key = key[idx+1:] + + switch t { + case claims.TypeAnonymous: + keys.disp = append(keys.disp, identity.IdentityDisplay{ + IdentityType: t, + Display: "Anonymous", + AvatarURL: dtos.GetGravatarUrl(fakeCfgForGravatar, string(t)), + }) + continue + case claims.TypeAPIKey: + keys.disp = append(keys.disp, identity.IdentityDisplay{ + IdentityType: t, + UID: key, + Display: "API Key", + AvatarURL: dtos.GetGravatarUrl(fakeCfgForGravatar, string(t)), + }) + continue + case claims.TypeProvisioning: + keys.disp = append(keys.disp, identity.IdentityDisplay{ + IdentityType: t, + UID: "Provisioning", + Display: "Provisioning", + AvatarURL: dtos.GetGravatarUrl(fakeCfgForGravatar, string(t)), + }) + continue + default: + // OK + } + } + + // Try reading the internal ID + id, err := strconv.ParseInt(key, 10, 64) + if err == nil { + if id == 0 { + keys.disp = append(keys.disp, identity.IdentityDisplay{ + IdentityType: claims.TypeUser, + UID: key, + Display: "System admin", + }) + continue + } + keys.ids = append(keys.ids, id) + } else { + keys.uids = append(keys.uids, key) + } + } + return keys +} diff --git a/pkg/registry/apis/identity/legacy/legacy_sql.go b/pkg/registry/apis/identity/legacy/legacy_sql.go new file mode 100644 index 00000000000..65c66172bfa --- /dev/null +++ b/pkg/registry/apis/identity/legacy/legacy_sql.go @@ -0,0 +1,186 @@ +package legacy + +import ( + "context" + "fmt" + "text/template" + + "github.com/grafana/authlib/claims" + "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" +) + +var ( + _ LegacyIdentityStore = (*legacySQLStore)(nil) +) + +type legacySQLStore struct { + dialect sqltemplate.Dialect + sql legacysql.NamespacedDBProvider + teamsRV legacysql.ResourceVersionLookup + usersRV legacysql.ResourceVersionLookup +} + +func NewLegacySQLStores(sql legacysql.NamespacedDBProvider) (LegacyIdentityStore, error) { + db, err := sql(context.Background()) + if err != nil { + return nil, err + } + dialect := sqltemplate.DialectForDriver(string(db.GetDBType())) + if dialect == nil { + return nil, fmt.Errorf("unknown dialect") + } + + return &legacySQLStore{ + sql: sql, + dialect: dialect, + teamsRV: legacysql.GetResourceVersionLookup(sql, "team", "updated"), + usersRV: legacysql.GetResourceVersionLookup(sql, "user", "updated"), + }, nil +} + +// 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 + query.OrgID = ns.OrgID + if ns.OrgID == 0 { + return nil, fmt.Errorf("expected non zero orgID") + } + + req := sqlQueryListTeams{ + SQLTemplate: sqltemplate.New(s.dialect), + Query: &query, + } + + rawQuery, err := sqltemplate.Execute(sqlQueryTeams, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeams.Name(), err) + } + q := rawQuery + + // fmt.Printf("%s // %v\n", rawQuery, req.GetArgs()) + + db, err := s.sql(ctx) + if err != nil { + return nil, err + } + + res := &ListTeamResult{} + rows, err := db.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + + 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 + } + } + if query.UID == "" { + res.RV, err = s.teamsRV(ctx) + } + } + 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 + } + + 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") + } + + return s.queryUsers(ctx, sqlQueryUsers, sqlQueryListUsers{ + SQLTemplate: sqltemplate.New(s.dialect), + Query: &query, + }, limit, query.UID != "") +} + +func (s *legacySQLStore) queryUsers(ctx context.Context, t *template.Template, req sqltemplate.ArgsIface, limit int, getRV bool) (*ListUserResult, error) { + rawQuery, err := sqltemplate.Execute(t, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlQueryUsers.Name(), err) + } + q := rawQuery + + // fmt.Printf("%s // %v\n", rawQuery, req.GetArgs()) + db, err := s.sql(ctx) + if err != nil { + return nil, err + } + + res := &ListUserResult{} + rows, err := db.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + + if err == nil { + var lastID int64 + for rows.Next() { + u := user.User{} + err = rows.Scan(&u.OrgID, &u.ID, &u.UID, &u.Login, &u.Email, &u.Name, + &u.Created, &u.Updated, &u.IsServiceAccount, &u.IsDisabled, &u.IsAdmin, + ) + if err != nil { + return res, err + } + lastID = u.ID + res.Users = append(res.Users, u) + if len(res.Users) > limit { + res.ContinueID = lastID + break + } + } + if getRV { + res.RV, err = s.usersRV(ctx) + } + } + return res, err +} + +// GetUserTeams implements LegacyIdentityStore. +func (s *legacySQLStore) GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error) { + panic("unimplemented") +} + +// GetDisplay implements LegacyIdentityStore. +func (s *legacySQLStore) GetDisplay(ctx context.Context, ns claims.NamespaceInfo, query GetUserDisplayQuery) (*ListUserResult, error) { + query.OrgID = ns.OrgID + if ns.OrgID == 0 { + return nil, fmt.Errorf("expected non zero orgID") + } + + return s.queryUsers(ctx, sqlQueryDisplay, sqlQueryGetDisplay{ + SQLTemplate: sqltemplate.New(s.dialect), + Query: &query, + }, 10000, false) +} diff --git a/pkg/registry/apis/identity/legacy/queries.go b/pkg/registry/apis/identity/legacy/queries.go new file mode 100644 index 00000000000..be9d88cdc9b --- /dev/null +++ b/pkg/registry/apis/identity/legacy/queries.go @@ -0,0 +1,58 @@ +package legacy + +import ( + "embed" + "fmt" + "text/template" + + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +// Templates setup. +var ( + //go:embed *.sql + sqlTemplatesFS embed.FS + + sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`)) +) + +func mustTemplate(filename string) *template.Template { + if t := sqlTemplates.Lookup(filename); t != nil { + return t + } + panic(fmt.Sprintf("template file not found: %s", filename)) +} + +// Templates. +var ( + sqlQueryTeams = mustTemplate("query_teams.sql") + sqlQueryUsers = mustTemplate("query_users.sql") + sqlQueryDisplay = mustTemplate("query_display.sql") +) + +type sqlQueryListUsers struct { + *sqltemplate.SQLTemplate + Query *ListUserQuery +} + +func (r sqlQueryListUsers) Validate() error { + return nil // TODO +} + +type sqlQueryListTeams struct { + *sqltemplate.SQLTemplate + Query *ListTeamQuery +} + +func (r sqlQueryListTeams) Validate() error { + return nil // TODO +} + +type sqlQueryGetDisplay struct { + *sqltemplate.SQLTemplate + Query *GetUserDisplayQuery +} + +func (r sqlQueryGetDisplay) Validate() error { + return nil // TODO +} diff --git a/pkg/registry/apis/identity/legacy/queries_test.go b/pkg/registry/apis/identity/legacy/queries_test.go new file mode 100644 index 00000000000..0e9dfabc167 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/queries_test.go @@ -0,0 +1,207 @@ +package legacy + +import ( + "embed" + "os" + "path/filepath" + "testing" + "text/template" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +//go:embed testdata/* +var testdataFS embed.FS + +func testdata(t *testing.T, filename string) []byte { + t.Helper() + b, err := testdataFS.ReadFile(`testdata/` + filename) + if err != nil { + writeTestData(filename, "") + assert.Fail(t, "missing test file") + } + return b +} + +func writeTestData(filename, value string) { + _ = os.WriteFile(filepath.Join("testdata", filename), []byte(value), 0777) +} + +func TestQueries(t *testing.T) { + t.Parallel() + + // Check each dialect + dialects := []sqltemplate.Dialect{ + sqltemplate.MySQL, + sqltemplate.SQLite, + sqltemplate.PostgreSQL, + } + + // Each template has one or more test cases, each identified with a + // descriptive name (e.g. "happy path", "error twiddling the frobb"). Each + // of them will test that for the same input data they must produce a result + // that will depend on the Dialect. Expected queries should be defined in + // separate files in the testdata directory. This improves the testing + // experience by separating test data from test code, since mixing both + // tends to make it more difficult to reason about what is being done, + // especially as we want testing code to scale and make it easy to add + // tests. + type ( + testCase = struct { + Name string + + // Data should be the struct passed to the template. + Data sqltemplate.SQLTemplateIface + } + ) + + // Define tests cases. Most templates are trivial and testing that they + // generate correct code for a single Dialect is fine, since the one thing + // that always changes is how SQL placeholder arguments are passed (most + // Dialects use `?` while PostgreSQL uses `$1`, `$2`, etc.), and that is + // something that should be tested in the Dialect implementation instead of + // here. We will ask to have at least one test per SQL template, and we will + // lean to test MySQL. Templates containing branching (conditionals, loops, + // etc.) should be exercised at least once in each of their branches. + // + // NOTE: in the Data field, make sure to have pointers populated to simulate + // data is set as it would be in a real request. The data being correctly + // populated in each case should be tested in integration tests, where the + // data will actually flow to and from a real database. In this tests we + // only care about producing the correct SQL. + testCases := map[*template.Template][]*testCase{ + sqlQueryTeams: { + { + Name: "teams_uid", + Data: &sqlQueryListTeams{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &ListTeamQuery{ + UID: "abc", + }, + }, + }, + { + Name: "teams_page_1", + Data: &sqlQueryListTeams{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &ListTeamQuery{ + Limit: 5, + }, + }, + }, + { + Name: "teams_page_2", + Data: &sqlQueryListTeams{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &ListTeamQuery{ + ContinueID: 1, + Limit: 2, + }, + }, + }, + }, + sqlQueryUsers: { + { + Name: "users_uid", + Data: &sqlQueryListUsers{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &ListUserQuery{ + UID: "abc", + }, + }, + }, + { + Name: "users_page_1", + Data: &sqlQueryListUsers{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &ListUserQuery{ + Limit: 5, + }, + }, + }, + { + Name: "users_page_2", + Data: &sqlQueryListUsers{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &ListUserQuery{ + ContinueID: 1, + Limit: 2, + }, + }, + }, + }, + sqlQueryDisplay: { + { + Name: "display_uids", + Data: &sqlQueryGetDisplay{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &GetUserDisplayQuery{ + OrgID: 2, + UIDs: []string{"a", "b"}, + }, + }, + }, + { + Name: "display_ids", + Data: &sqlQueryGetDisplay{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &GetUserDisplayQuery{ + OrgID: 2, + IDs: []int64{1, 2}, + }, + }, + }, + { + Name: "display_ids_uids", + Data: &sqlQueryGetDisplay{ + SQLTemplate: new(sqltemplate.SQLTemplate), + Query: &GetUserDisplayQuery{ + OrgID: 2, + UIDs: []string{"a", "b"}, + IDs: []int64{1, 2}, + }, + }, + }, + }, + } + + // Execute test cases + for tmpl, tcs := range testCases { + t.Run(tmpl.Name(), func(t *testing.T) { + t.Parallel() + + for _, tc := range tcs { + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + for _, dialect := range dialects { + filename := dialect.DialectName() + "__" + tc.Name + ".sql" + t.Run(filename, func(t *testing.T) { + // not parallel because we're sharing tc.Data, not + // worth it deep cloning + + expectedQuery := string(testdata(t, filename)) + //expectedQuery := sqltemplate.FormatSQL(rawQuery) + + tc.Data.SetDialect(dialect) + err := tc.Data.Validate() + require.NoError(t, err) + got, err := sqltemplate.Execute(tmpl, tc.Data) + require.NoError(t, err) + + got = sqltemplate.RemoveEmptyLines(got) + if diff := cmp.Diff(expectedQuery, got); diff != "" { + writeTestData(filename, got) + t.Errorf("%s: %s", tc.Name, diff) + } + }) + } + }) + } + }) + } +} diff --git a/pkg/registry/apis/identity/legacy/query_display.sql b/pkg/registry/apis/identity/legacy/query_display.sql new file mode 100644 index 00000000000..51293d76f17 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/query_display.sql @@ -0,0 +1,13 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM {{ .Ident "user" }} as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = {{ .Arg .Query.OrgID }} AND ( 1=2 +{{ if .Query.UIDs }} + OR uid IN ({{ .ArgList .Query.UIDs }}) +{{ end }} +{{ if .Query.IDs }} + OR u.id IN ({{ .ArgList .Query.IDs }}) +{{ end }} + ) + ORDER BY u.id asc + LIMIT 500 \ No newline at end of file diff --git a/pkg/registry/apis/identity/legacy/query_teams.sql b/pkg/registry/apis/identity/legacy/query_teams.sql new file mode 100644 index 00000000000..4a535530ac0 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/query_teams.sql @@ -0,0 +1,11 @@ +SELECT id, uid, name, email, created, updated + FROM {{ .Ident "team" }} + WHERE org_id = {{ .Arg .Query.OrgID }} +{{ if .Query.UID }} + AND uid = {{ .Arg .Query.UID }} +{{ end }} +{{ if .Query.ContinueID }} + AND id > {{ .Arg .Query.ContinueID }} +{{ end }} + ORDER BY id asc + LIMIT {{ .Arg .Query.Limit }} \ No newline at end of file diff --git a/pkg/registry/apis/identity/legacy/query_users.sql b/pkg/registry/apis/identity/legacy/query_users.sql new file mode 100644 index 00000000000..03e66270c7f --- /dev/null +++ b/pkg/registry/apis/identity/legacy/query_users.sql @@ -0,0 +1,13 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM {{ .Ident "user" }} as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = {{ .Arg .Query.OrgID }} + AND u.is_service_account = {{ .Arg .Query.IsServiceAccount }} +{{ if .Query.UID }} + AND uid = {{ .Arg .Query.UID }} +{{ end }} +{{ if .Query.ContinueID }} + AND id > {{ .Arg .Query.ContinueID }} +{{ end }} + ORDER BY u.id asc + LIMIT {{ .Arg .Query.Limit }} \ No newline at end of file diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__display_ids.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__display_ids.sql new file mode 100755 index 00000000000..bff3eddd1f9 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__display_ids.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM `user` as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? AND ( 1=2 + OR u.id IN (?, ?) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__display_ids_uids.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__display_ids_uids.sql new file mode 100755 index 00000000000..ab8d701ba34 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__display_ids_uids.sql @@ -0,0 +1,9 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM `user` as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? AND ( 1=2 + OR uid IN (?, ?) + OR u.id IN (?, ?) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__display_uids.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__display_uids.sql new file mode 100755 index 00000000000..10786ade099 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__display_uids.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM `user` as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? AND ( 1=2 + OR uid IN (?, ?) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_1.sql new file mode 100755 index 00000000000..56ac17f8539 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_1.sql @@ -0,0 +1,5 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = ? + ORDER BY id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_2.sql new file mode 100755 index 00000000000..d520548b8aa --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_2.sql @@ -0,0 +1,6 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = ? + AND id > ? + ORDER BY id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__teams_uid.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__teams_uid.sql new file mode 100755 index 00000000000..2e88bccd126 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__teams_uid.sql @@ -0,0 +1,6 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = ? + AND uid = ? + ORDER BY id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__users_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__users_page_1.sql new file mode 100755 index 00000000000..e93a3522534 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__users_page_1.sql @@ -0,0 +1,7 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM `user` as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? + AND u.is_service_account = ? + ORDER BY u.id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__users_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__users_page_2.sql new file mode 100755 index 00000000000..1eed7fec311 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__users_page_2.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM `user` as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? + AND u.is_service_account = ? + AND id > ? + ORDER BY u.id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/mysql__users_uid.sql b/pkg/registry/apis/identity/legacy/testdata/mysql__users_uid.sql new file mode 100755 index 00000000000..1d3b1c62fcb --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/mysql__users_uid.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM `user` as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? + AND u.is_service_account = ? + AND uid = ? + ORDER BY u.id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__display_ids.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__display_ids.sql new file mode 100755 index 00000000000..2f7693b762a --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__display_ids.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = $1 AND ( 1=2 + OR u.id IN ($2, $3) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__display_ids_uids.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__display_ids_uids.sql new file mode 100755 index 00000000000..e26b4f4fde4 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__display_ids_uids.sql @@ -0,0 +1,9 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = $1 AND ( 1=2 + OR uid IN ($2, $3) + OR u.id IN ($4, $5) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__display_uids.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__display_uids.sql new file mode 100755 index 00000000000..487b6591c76 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__display_uids.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = $1 AND ( 1=2 + OR uid IN ($2, $3) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_1.sql new file mode 100755 index 00000000000..f36df0cf98b --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_1.sql @@ -0,0 +1,5 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = $1 + ORDER BY id asc + LIMIT $2 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_2.sql new file mode 100755 index 00000000000..6afdd3c7613 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_2.sql @@ -0,0 +1,6 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = $1 + AND id > $2 + ORDER BY id asc + LIMIT $3 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__teams_uid.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__teams_uid.sql new file mode 100755 index 00000000000..1d009e16123 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__teams_uid.sql @@ -0,0 +1,6 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = $1 + AND uid = $2 + ORDER BY id asc + LIMIT $3 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__users_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__users_page_1.sql new file mode 100755 index 00000000000..74d102b6e4a --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__users_page_1.sql @@ -0,0 +1,7 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = $1 + AND u.is_service_account = $2 + ORDER BY u.id asc + LIMIT $3 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__users_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__users_page_2.sql new file mode 100755 index 00000000000..df6b238e9e2 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__users_page_2.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = $1 + AND u.is_service_account = $2 + AND id > $3 + ORDER BY u.id asc + LIMIT $4 diff --git a/pkg/registry/apis/identity/legacy/testdata/postgres__users_uid.sql b/pkg/registry/apis/identity/legacy/testdata/postgres__users_uid.sql new file mode 100755 index 00000000000..cfbfe5af9cf --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/postgres__users_uid.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = $1 + AND u.is_service_account = $2 + AND uid = $3 + ORDER BY u.id asc + LIMIT $4 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids.sql new file mode 100755 index 00000000000..70280aca234 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? AND ( 1=2 + OR u.id IN (?, ?) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids_uids.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids_uids.sql new file mode 100755 index 00000000000..18ea9b0d4ef --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids_uids.sql @@ -0,0 +1,9 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? AND ( 1=2 + OR uid IN (?, ?) + OR u.id IN (?, ?) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__display_uids.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__display_uids.sql new file mode 100755 index 00000000000..9140b071fb1 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__display_uids.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? AND ( 1=2 + OR uid IN (?, ?) + ) + ORDER BY u.id asc + LIMIT 500 diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_1.sql new file mode 100755 index 00000000000..56ac17f8539 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_1.sql @@ -0,0 +1,5 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = ? + ORDER BY id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_2.sql new file mode 100755 index 00000000000..d520548b8aa --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_2.sql @@ -0,0 +1,6 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = ? + AND id > ? + ORDER BY id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_uid.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_uid.sql new file mode 100755 index 00000000000..2e88bccd126 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__teams_uid.sql @@ -0,0 +1,6 @@ +SELECT id, uid, name, email, created, updated + FROM "team" + WHERE org_id = ? + AND uid = ? + ORDER BY id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_1.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_1.sql new file mode 100755 index 00000000000..2b509ee1617 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_1.sql @@ -0,0 +1,7 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? + AND u.is_service_account = ? + ORDER BY u.id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_2.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_2.sql new file mode 100755 index 00000000000..703f36d44fc --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_2.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? + AND u.is_service_account = ? + AND id > ? + ORDER BY u.id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/testdata/sqlite__users_uid.sql b/pkg/registry/apis/identity/legacy/testdata/sqlite__users_uid.sql new file mode 100755 index 00000000000..32f04d7b7c2 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/testdata/sqlite__users_uid.sql @@ -0,0 +1,8 @@ +SELECT org_user.org_id, u.id, u.uid, u.login, u.email, u.name, + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + FROM "user" as u JOIN org_user ON u.id = org_user.user_id + WHERE org_user.org_id = ? + AND u.is_service_account = ? + AND uid = ? + ORDER BY u.id asc + LIMIT ? diff --git a/pkg/registry/apis/identity/legacy/types.go b/pkg/registry/apis/identity/legacy/types.go new file mode 100644 index 00000000000..289e5176363 --- /dev/null +++ b/pkg/registry/apis/identity/legacy/types.go @@ -0,0 +1,50 @@ +package legacy + +import ( + "context" + + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/services/user" +) + +type ListUserQuery struct { + OrgID int64 + UID string + ContinueID int64 // ContinueID + Limit int64 + IsServiceAccount bool +} + +type ListUserResult struct { + Users []user.User + ContinueID int64 + RV int64 +} + +type ListTeamQuery struct { + OrgID int64 + UID string + ContinueID int64 // ContinueID + Limit int64 +} + +type ListTeamResult struct { + Teams []team.Team + ContinueID int64 + RV int64 +} + +type GetUserDisplayQuery struct { + OrgID int64 + UIDs []string + IDs []int64 +} + +// 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) + ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error) + GetDisplay(ctx context.Context, ns claims.NamespaceInfo, query GetUserDisplayQuery) (*ListUserResult, error) + GetUserTeams(ctx context.Context, ns claims.NamespaceInfo, uid string) ([]team.Team, error) +} diff --git a/pkg/registry/apis/identity/legacy_sa.go b/pkg/registry/apis/identity/legacy_sa.go index bec879c586b..606b3a6b6a5 100644 --- a/pkg/registry/apis/identity/legacy_sa.go +++ b/pkg/registry/apis/identity/legacy_sa.go @@ -5,16 +5,16 @@ import ( "fmt" "strconv" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "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" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/user" ) var ( @@ -26,7 +26,7 @@ var ( ) type legacyServiceAccountStorage struct { - service user.Service + service legacy.LegacyIdentityStore tableConverter rest.TableConvertor resourceInfo common.ResourceInfo } @@ -58,7 +58,7 @@ func (s *legacyServiceAccountStorage) List(ctx context.Context, options *interna if err != nil { return nil, err } - query := &user.ListUsersCommand{ + query := legacy.ListUserQuery{ OrgID: ns.OrgID, Limit: options.Limit, IsServiceAccount: true, @@ -70,13 +70,14 @@ func (s *legacyServiceAccountStorage) List(ctx context.Context, options *interna } } - found, err := s.service.List(ctx, query) + found, err := s.service.ListUsers(ctx, ns, query) if err != nil { return nil, err } + list := &identity.ServiceAccountList{} for _, item := range found.Users { - list.Items = append(list.Items, *toSAItem(item, ns.Value)) + list.Items = append(list.Items, *toSAItem(&item, ns.Value)) } if found.ContinueID > 0 { list.ListMeta.Continue = strconv.FormatInt(found.ContinueID, 10) @@ -116,15 +117,18 @@ func (s *legacyServiceAccountStorage) Get(ctx context.Context, name string, opti if err != nil { return nil, err } - found, err := s.service.GetByUID(ctx, &user.GetUserByUIDQuery{ - OrgID: ns.OrgID, - UID: name, - }) + query := legacy.ListUserQuery{ + OrgID: ns.OrgID, + Limit: 1, + IsServiceAccount: true, + } + + found, err := s.service.ListUsers(ctx, ns, query) if found == nil || err != nil { return nil, s.resourceInfo.NewNotFound(name) } - if !found.IsServiceAccount { - return nil, s.resourceInfo.NewNotFound(name) // looking up the wrong type + if len(found.Users) < 1 { + return nil, s.resourceInfo.NewNotFound(name) } - return toUserItem(found, ns.Value), nil + return toSAItem(&found.Users[0], ns.Value), nil } diff --git a/pkg/registry/apis/identity/legacy_teams.go b/pkg/registry/apis/identity/legacy_teams.go index b69ae7621d7..bdcb84550fd 100644 --- a/pkg/registry/apis/identity/legacy_teams.go +++ b/pkg/registry/apis/identity/legacy_teams.go @@ -4,16 +4,17 @@ import ( "context" "strconv" + "github.com/grafana/authlib/claims" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "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" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/team" ) var ( @@ -25,7 +26,7 @@ var ( ) type legacyTeamStorage struct { - service team.Service + service legacy.LegacyIdentityStore tableConverter rest.TableConvertor resourceInfo common.ResourceInfo } @@ -52,20 +53,25 @@ func (s *legacyTeamStorage) ConvertToTable(ctx context.Context, object runtime.O return s.tableConverter.ConvertToTable(ctx, object, tableOptions) } -func (s *legacyTeamStorage) doList(ctx context.Context, ns string, query *team.ListTeamsCommand) (*identity.TeamList, error) { +func (s *legacyTeamStorage) doList(ctx context.Context, ns claims.NamespaceInfo, query legacy.ListTeamQuery) (*identity.TeamList, error) { if query.Limit < 1 { query.Limit = 100 } - teams, err := s.service.ListTeams(ctx, query) + + rsp, err := s.service.ListTeams(ctx, ns, query) if err != nil { return nil, err } - list := &identity.TeamList{} - for _, team := range teams { + list := &identity.TeamList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: strconv.FormatInt(rsp.RV, 10), + }, + } + for _, team := range rsp.Teams { item := identity.Team{ ObjectMeta: metav1.ObjectMeta{ Name: team.UID, - Namespace: ns, + Namespace: ns.Value, CreationTimestamp: metav1.NewTime(team.Created), ResourceVersion: strconv.FormatInt(team.Updated.UnixMilli(), 10), }, @@ -85,6 +91,9 @@ func (s *legacyTeamStorage) doList(ctx context.Context, ns string, query *team.L }) list.Items = append(list.Items, item) } + if rsp.ContinueID > 0 { + list.ListMeta.Continue = strconv.FormatInt(rsp.ContinueID, 10) + } return list, nil } @@ -93,10 +102,17 @@ func (s *legacyTeamStorage) List(ctx context.Context, options *internalversion.L if err != nil { return nil, err } - return s.doList(ctx, ns.Value, &team.ListTeamsCommand{ - Limit: int(options.Limit), + 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) } func (s *legacyTeamStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { @@ -104,9 +120,9 @@ func (s *legacyTeamStorage) Get(ctx context.Context, name string, options *metav if err != nil { return nil, err } - rsp, err := s.doList(ctx, ns.Value, &team.ListTeamsCommand{ - Limit: 1, + rsp, err := s.doList(ctx, ns, legacy.ListTeamQuery{ OrgID: ns.OrgID, + Limit: 1, UID: name, }) if err != nil { @@ -117,3 +133,28 @@ func (s *legacyTeamStorage) Get(ctx context.Context, name string, options *metav } return nil, s.resourceInfo.NewNotFound(name) } + +func asTeam(team *team.Team, ns string) (*identity.Team, error) { + item := &identity.Team{ + ObjectMeta: metav1.ObjectMeta{ + Name: team.UID, + Namespace: ns, + CreationTimestamp: metav1.NewTime(team.Created), + ResourceVersion: strconv.FormatInt(team.Updated.UnixMilli(), 10), + }, + Spec: identity.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/legacy_user_teams.go b/pkg/registry/apis/identity/legacy_user_teams.go new file mode 100644 index 00000000000..c164ba4dbce --- /dev/null +++ b/pkg/registry/apis/identity/legacy_user_teams.go @@ -0,0 +1,87 @@ +package identity + +import ( + "context" + "net/http" + + identity "github.com/grafana/grafana/pkg/apimachinery/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" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" +) + +type userTeamsREST struct { + logger log.Logger + store legacy.LegacyIdentityStore +} + +var ( + _ rest.Storage = (*userTeamsREST)(nil) + _ rest.SingularNameProvider = (*userTeamsREST)(nil) + _ rest.Connecter = (*userTeamsREST)(nil) + _ rest.Scoper = (*userTeamsREST)(nil) + _ rest.StorageMetadata = (*userTeamsREST)(nil) +) + +func newUserTeamsREST(store legacy.LegacyIdentityStore) *userTeamsREST { + return &userTeamsREST{ + logger: log.New("user teams"), + store: store, + } +} + +func (r *userTeamsREST) New() runtime.Object { + return &identity.TeamList{} +} + +func (r *userTeamsREST) Destroy() {} + +func (r *userTeamsREST) NamespaceScoped() bool { + return true +} + +func (r *userTeamsREST) GetSingularName() string { + return "TeamList" // Used for the +} + +func (r *userTeamsREST) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} // and parquet! +} + +func (r *userTeamsREST) ProducesObject(verb string) interface{} { + return &identity.TeamList{} +} + +func (r *userTeamsREST) ConnectMethods() []string { + return []string{"GET"} +} + +func (r *userTeamsREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" // true means you can use the trailing path as a variable +} + +func (r *userTeamsREST) 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 := &identity.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/legacy_users.go b/pkg/registry/apis/identity/legacy_users.go index b7a66e1cc81..321e755c8fa 100644 --- a/pkg/registry/apis/identity/legacy_users.go +++ b/pkg/registry/apis/identity/legacy_users.go @@ -5,16 +5,16 @@ import ( "fmt" "strconv" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "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" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/user" ) var ( @@ -26,7 +26,7 @@ var ( ) type legacyUserStorage struct { - service user.Service + service legacy.LegacyIdentityStore tableConverter rest.TableConvertor resourceInfo common.ResourceInfo } @@ -58,9 +58,10 @@ func (s *legacyUserStorage) List(ctx context.Context, options *internalversion.L if err != nil { return nil, err } - query := &user.ListUsersCommand{ - OrgID: ns.OrgID, - Limit: options.Limit, + query := legacy.ListUserQuery{ + OrgID: ns.OrgID, + Limit: options.Limit, + IsServiceAccount: false, } if options.Continue != "" { query.ContinueID, err = strconv.ParseInt(options.Continue, 10, 64) @@ -69,13 +70,14 @@ func (s *legacyUserStorage) List(ctx context.Context, options *internalversion.L } } - found, err := s.service.List(ctx, query) + found, err := s.service.ListUsers(ctx, ns, query) if err != nil { return nil, err } + list := &identity.UserList{} for _, item := range found.Users { - list.Items = append(list.Items, *toUserItem(item, ns.Value)) + list.Items = append(list.Items, *toUserItem(&item, ns.Value)) } if found.ContinueID > 0 { list.ListMeta.Continue = strconv.FormatInt(found.ContinueID, 10) @@ -116,15 +118,18 @@ func (s *legacyUserStorage) Get(ctx context.Context, name string, options *metav if err != nil { return nil, err } - found, err := s.service.GetByUID(ctx, &user.GetUserByUIDQuery{ - OrgID: ns.OrgID, - UID: name, - }) + query := legacy.ListUserQuery{ + OrgID: ns.OrgID, + Limit: 1, + IsServiceAccount: false, + } + + found, err := s.service.ListUsers(ctx, ns, query) if found == nil || err != nil { return nil, s.resourceInfo.NewNotFound(name) } - if found.IsServiceAccount { - return nil, s.resourceInfo.NewNotFound(name) // looking up the wrong type + if len(found.Users) < 1 { + return nil, s.resourceInfo.NewNotFound(name) } - return toUserItem(found, ns.Value), nil + return toUserItem(&found.Users[0], ns.Value), nil } diff --git a/pkg/registry/apis/identity/register.go b/pkg/registry/apis/identity/register.go index 38a8869e052..3e06dd3fa5e 100644 --- a/pkg/registry/apis/identity/register.go +++ b/pkg/registry/apis/identity/register.go @@ -16,37 +16,42 @@ import ( identity "github.com/grafana/grafana/pkg/apimachinery/apis/identity/v0alpha1" identityapi "github.com/grafana/grafana/pkg/apimachinery/identity" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/registry/apis/identity/legacy" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/team" - "github.com/grafana/grafana/pkg/services/user" ) var _ builder.APIGroupBuilder = (*IdentityAPIBuilder)(nil) // This is used just so wire has something unique to return type IdentityAPIBuilder struct { - svcTeam team.Service - svcUser user.Service + Store legacy.LegacyIdentityStore } func RegisterAPIService( features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, - svcTeam team.Service, - svcUser user.Service, - -) *IdentityAPIBuilder { + // svcTeam team.Service, + // svcUser user.Service, + sql db.DB, +) (*IdentityAPIBuilder, error) { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil // skip registration unless opting into experimental apis + return nil, nil // skip registration unless opting into experimental apis + } + + store, err := legacy.NewLegacySQLStores(func(context.Context) (db.DB, error) { + return sql, nil + }) + if err != nil { + return nil, err } builder := &IdentityAPIBuilder{ - svcTeam: svcTeam, - svcUser: svcUser, + Store: store, } apiregistration.RegisterAPI(builder) - return builder + return builder, nil } func (b *IdentityAPIBuilder) GetGroupVersion() schema.GroupVersion { @@ -84,7 +89,7 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo( team := identity.TeamResourceInfo teamStore := &legacyTeamStorage{ - service: b.svcTeam, + service: b.Store, resourceInfo: team, tableConverter: team.TableConverter(), } @@ -92,20 +97,24 @@ func (b *IdentityAPIBuilder) GetAPIGroupInfo( user := identity.UserResourceInfo userStore := &legacyUserStorage{ - service: b.svcUser, + service: b.Store, resourceInfo: user, tableConverter: user.TableConverter(), } storage[user.StoragePath()] = userStore + storage[user.StoragePath("teams")] = newUserTeamsREST(b.Store) sa := identity.ServiceAccountResourceInfo saStore := &legacyServiceAccountStorage{ - service: b.svcUser, + service: b.Store, resourceInfo: sa, tableConverter: sa.TableConverter(), } storage[sa.StoragePath()] = saStore + // The display endpoint -- NOTE, this uses a rewrite hack to allow requests without a name parameter + storage["display"] = newDisplayREST(b.Store) + apiGroupInfo.VersionedResourcesStorageMap[identity.VERSION] = storage return &apiGroupInfo, nil } diff --git a/pkg/services/apiserver/auth/authorizer/org_id.go b/pkg/services/apiserver/auth/authorizer/org_id.go index f35bbce484e..5eaace081d4 100644 --- a/pkg/services/apiserver/auth/authorizer/org_id.go +++ b/pkg/services/apiserver/auth/authorizer/org_id.go @@ -41,6 +41,11 @@ func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut return authorizer.DecisionNoOpinion, "", nil } + // Grafana super admins can see things in every org + if signedInUser.GetIsGrafanaAdmin() { + return authorizer.DecisionNoOpinion, "", nil + } + if info.OrgID == -1 { return authorizer.DecisionDeny, "org id is required", nil } diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index f3a489ef9a8..376df20edbd 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -43,6 +43,12 @@ var pathRewriters = []filters.PathRewriter{ return matches[1] + "/name" // connector requires a name }, }, + { + Pattern: regexp.MustCompile(`(/apis/identity.grafana.app/v0alpha1/namespaces/.*/display$)`), + ReplaceFunc: func(matches []string) string { + return matches[1] + "/name" // connector requires a name + }, + }, } func SetupConfig( diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index a609202dc68..8bd371de36a 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -90,13 +90,6 @@ type SearchTeamsQuery struct { HiddenUsers map[string]struct{} } -type ListTeamsCommand struct { - Limit int - Start int - OrgID int64 - UID string -} - type TeamDTO struct { ID int64 `json:"id" xorm:"id"` UID string `json:"uid" xorm:"uid"` diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 5ae7abb2857..3a44a8883fa 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -8,7 +8,6 @@ type Service interface { CreateTeam(ctx context.Context, name, email string, orgID int64) (Team, error) UpdateTeam(ctx context.Context, cmd *UpdateTeamCommand) error DeleteTeam(ctx context.Context, cmd *DeleteTeamCommand) error - ListTeams(ctx context.Context, query *ListTeamsCommand) ([]*Team, error) SearchTeams(ctx context.Context, query *SearchTeamsQuery) (SearchTeamQueryResult, error) GetTeamByID(ctx context.Context, query *GetTeamByIDQuery) (*TeamDTO, error) GetTeamsByUser(ctx context.Context, query *GetTeamsByUserQuery) ([]*TeamDTO, error) diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index 4cd5777937f..8f3db689fd3 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -20,7 +20,6 @@ type store interface { Create(name, email string, orgID int64) (team.Team, error) Update(ctx context.Context, cmd *team.UpdateTeamCommand) error Delete(ctx context.Context, cmd *team.DeleteTeamCommand) error - ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) Search(ctx context.Context, query *team.SearchTeamsQuery) (team.SearchTeamQueryResult, error) GetByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) GetByUser(ctx context.Context, query *team.GetTeamsByUserQuery) ([]*team.TeamDTO, error) @@ -268,21 +267,6 @@ func (ss *xormStore) Search(ctx context.Context, query *team.SearchTeamsQuery) ( return queryResult, nil } -func (ss *xormStore) ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) { - results := make([]*team.Team, 0) - err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { - q := sess.Table("team") - q.Where("team.org_id=?", query.OrgID) - if query.UID != "" { - q.Where("team.uid=?", query.UID) - } - q.Limit(query.Limit, query.Start) - - return q.Find(&results) - }) - return results, err -} - func (ss *xormStore) GetByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) { var queryResult *team.TeamDTO err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { diff --git a/pkg/services/team/teamimpl/team.go b/pkg/services/team/teamimpl/team.go index 627d4f75acd..e7c9537b751 100644 --- a/pkg/services/team/teamimpl/team.go +++ b/pkg/services/team/teamimpl/team.go @@ -51,14 +51,6 @@ func (s *Service) DeleteTeam(ctx context.Context, cmd *team.DeleteTeamCommand) e return s.store.Delete(ctx, cmd) } -func (s *Service) ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) { - ctx, span := s.tracer.Start(ctx, "team.ListTeams", trace.WithAttributes( - attribute.Int64("orgID", query.OrgID), - )) - defer span.End() - return s.store.ListTeams(ctx, query) -} - func (s *Service) SearchTeams(ctx context.Context, query *team.SearchTeamsQuery) (team.SearchTeamQueryResult, error) { ctx, span := s.tracer.Start(ctx, "team.SearchTeams", trace.WithAttributes( attribute.Int64("orgID", query.OrgID), diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index 5a2e510c5d1..a561b7c4772 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -36,10 +36,6 @@ func (s *FakeService) SearchTeams(ctx context.Context, query *team.SearchTeamsQu return team.SearchTeamQueryResult{}, s.ExpectedError } -func (s *FakeService) ListTeams(ctx context.Context, query *team.ListTeamsCommand) ([]*team.Team, error) { - return nil, s.ExpectedError -} - func (s *FakeService) GetTeamByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) { return s.ExpectedTeamDTO, s.ExpectedError } diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index 8a4db77f29c..fe9d6e75cc7 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -98,13 +98,6 @@ type UpdateUserLastSeenAtCommand struct { OrgID int64 } -type ListUsersCommand struct { - OrgID int64 - Limit int64 - ContinueID int64 - IsServiceAccount bool -} - type ListUserResult struct { Users []*User ContinueID int64 diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go index 56f6b819523..69df82b5b2c 100644 --- a/pkg/services/user/user.go +++ b/pkg/services/user/user.go @@ -16,7 +16,6 @@ type Service interface { GetByUID(context.Context, *GetUserByUIDQuery) (*User, error) GetByLogin(context.Context, *GetUserByLoginQuery) (*User, error) GetByEmail(context.Context, *GetUserByEmailQuery) (*User, error) - List(context.Context, *ListUsersCommand) (*ListUserResult, error) Update(context.Context, *UpdateUserCommand) error UpdateLastSeenAt(context.Context, *UpdateUserLastSeenAtCommand) error GetSignedInUser(context.Context, *GetSignedInUserQuery) (*SignedInUser, error) diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index fe38e8c9ad7..5cfc775d16d 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -23,7 +23,6 @@ type store interface { GetByUID(ctx context.Context, orgId int64, uid string) (*user.User, error) GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error) GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error) - List(context.Context, *user.ListUsersCommand) (*user.ListUserResult, error) Delete(context.Context, int64) error LoginConflict(ctx context.Context, login, email string) error Update(context.Context, *user.UpdateUserCommand) error @@ -579,40 +578,6 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* return &result, err } -func (ss *sqlStore) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { - limit := int(query.Limit) - if limit <= 0 { - limit = 25 - } - result := &user.ListUserResult{ - Users: make([]*user.User, 0), - } - max := "" - err := ss.db.WithDbSession(ctx, func(dbSess *db.Session) error { - sess := dbSess.Table("user") - sess.Where("id >= ? AND is_service_account = ?", query.ContinueID, query.IsServiceAccount) - err := sess.OrderBy("id asc").Limit(limit + 1).Find(&result.Users) - if err != nil { - return err - } - - // Set the revision version - _, err = dbSess.Table("user").Select("MAX(updated)").Get(&max) - return err - }) - if max != "" { - t, err := time.Parse(time.DateTime, max) - if err == nil { - result.RV = t.UnixMilli() - } - } - if len(result.Users) > limit { - result.ContinueID = result.Users[limit].ID - result.Users = result.Users[:limit] - } - return result, err -} - func setOptional[T any](v *T, add func(v T)) { if v != nil { add(*v) diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index 64db9fdc323..e50855b48b5 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -378,15 +378,6 @@ func (s *Service) getSignedInUser(ctx context.Context, query *user.GetSignedInUs return usr, err } -func (s *Service) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { - ctx, span := s.tracer.Start(ctx, "user.List", trace.WithAttributes( - attribute.Int64("orgID", query.OrgID), - )) - defer span.End() - - return s.store.List(ctx, query) -} - func (s *Service) Search(ctx context.Context, query *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) { ctx, span := s.tracer.Start(ctx, "user.Search", trace.WithAttributes( attribute.Int64("orgID", query.OrgID), diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index 1b82d8c3f5e..3c8fcdfa56c 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -331,10 +331,6 @@ func (f *FakeUserStore) Search(ctx context.Context, query *user.SearchUsersQuery return f.ExpectedSearchUserQueryResult, f.ExpectedError } -func (f *FakeUserStore) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { - return nil, f.ExpectedError -} - func (f *FakeUserStore) Count(ctx context.Context) (int64, error) { return 0, nil } diff --git a/pkg/services/user/usertest/fake.go b/pkg/services/user/usertest/fake.go index 4dc6bee11ca..09a456d81d5 100644 --- a/pkg/services/user/usertest/fake.go +++ b/pkg/services/user/usertest/fake.go @@ -98,10 +98,6 @@ func (f *FakeUserService) Search(ctx context.Context, query *user.SearchUsersQue return &f.ExpectedSearchUsers, f.ExpectedError } -func (f *FakeUserService) List(ctx context.Context, query *user.ListUsersCommand) (*user.ListUserResult, error) { - return &f.ExpectedListUsers, f.ExpectedError -} - func (f *FakeUserService) BatchDisableUsers(ctx context.Context, cmd *user.BatchDisableUsersCommand) error { if f.BatchDisableUsersFn != nil { return f.BatchDisableUsersFn(ctx, cmd) diff --git a/pkg/services/user/usertest/mock.go b/pkg/services/user/usertest/mock.go index 504f03c1aed..348d1040997 100644 --- a/pkg/services/user/usertest/mock.go +++ b/pkg/services/user/usertest/mock.go @@ -310,36 +310,6 @@ func (_m *MockService) GetUsageStats(ctx context.Context) map[string]interface{} return r0 } -// List provides a mock function with given fields: _a0, _a1 -func (_m *MockService) List(_a0 context.Context, _a1 *user.ListUsersCommand) (*user.ListUserResult, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for List") - } - - var r0 *user.ListUserResult - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *user.ListUsersCommand) (*user.ListUserResult, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *user.ListUsersCommand) *user.ListUserResult); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*user.ListUserResult) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *user.ListUsersCommand) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Search provides a mock function with given fields: _a0, _a1 func (_m *MockService) Search(_a0 context.Context, _a1 *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) { ret := _m.Called(_a0, _a1) diff --git a/pkg/storage/legacysql/README.md b/pkg/storage/legacysql/README.md new file mode 100644 index 00000000000..06ab3375f23 --- /dev/null +++ b/pkg/storage/legacysql/README.md @@ -0,0 +1,8 @@ +# Legacy SQL + +As we transition from our internal sql store towards unified storage, we can sometimes use existing +services to implement a k8s compatible storage that can then dual write into unified storage. + +However sometimes it is more efficient and cleaner to write explicit SQL commands designed for this goal. + +This package provides some helper functions to make this easier. \ No newline at end of file diff --git a/pkg/storage/legacysql/rv.go b/pkg/storage/legacysql/rv.go new file mode 100644 index 00000000000..a38fa4c9ea3 --- /dev/null +++ b/pkg/storage/legacysql/rv.go @@ -0,0 +1,51 @@ +package legacysql + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +// The database may depend on the request context +type NamespacedDBProvider func(ctx context.Context) (db.DB, error) + +// Get the list RV from the maximum updated time +type ResourceVersionLookup = func(ctx context.Context) (int64, error) + +// Get a resource version from the max value the updated field +func GetResourceVersionLookup(sql NamespacedDBProvider, table string, column string) ResourceVersionLookup { + return func(ctx context.Context) (int64, error) { + db, err := sql(ctx) + if err != nil { + return 1, err + } + + table = db.GetDialect().Quote(table) + column = db.GetDialect().Quote(column) + switch db.GetDBType() { + case migrator.Postgres: + max := time.Now() + err := db.GetSqlxSession().Get(ctx, &max, "SELECT MAX("+column+") FROM "+table) + if err != nil { + return 1, nil + } + return max.UnixMilli(), nil + case migrator.MySQL: + max := int64(1) + _ = db.GetSqlxSession().Get(ctx, &max, "SELECT UNIX_TIMESTAMP(MAX("+column+")) FROM "+table) + return max, nil + default: + // fallthrough to string version + } + + max := "" + err = db.GetSqlxSession().Get(ctx, &max, "SELECT MAX("+column+") FROM "+table) + if err == nil && max != "" { + t, _ := time.Parse(time.DateTime, max) // ignore null errors + return t.UnixMilli(), nil + } + return 1, nil + } +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 080dc5e8715..0e96f455238 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -38,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" + "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" @@ -49,7 +50,7 @@ const Org1 = "Org1" type K8sTestHelper struct { t *testing.T env server.TestEnv - namespacer request.NamespaceMapper + Namespacer request.NamespaceMapper Org1 OrgUsers // default OrgB OrgUsers // some other id @@ -66,7 +67,7 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { c := &K8sTestHelper{ env: *env, t: t, - namespacer: request.GetNamespaceMapper(nil), + Namespacer: request.GetNamespaceMapper(nil), } c.Org1 = c.createTestUsers(Org1) @@ -120,7 +121,7 @@ func (c *K8sTestHelper) GetResourceClient(args ResourceClientArgs) *K8sResourceC c.t.Helper() if args.Namespace == "" { - args.Namespace = c.namespacer(args.User.Identity.GetOrgID()) + args.Namespace = c.Namespacer(args.User.Identity.GetOrgID()) } client, err := dynamic.NewForConfig(args.User.NewRestConfig()) @@ -147,8 +148,46 @@ func (c *K8sTestHelper) AsStatusError(err error) *errors.StatusError { return statusError } +func (c *K8sResourceClient) SanitizeJSONList(v *unstructured.UnstructuredList, replaceMeta ...string) string { + c.t.Helper() + + clean := &unstructured.UnstructuredList{} + for _, item := range v.Items { + copy := c.sanitizeObject(&item, replaceMeta...) + clean.Items = append(clean.Items, *copy) + } + + out, err := json.MarshalIndent(clean, "", " ") + require.NoError(c.t, err) + return string(out) +} + +func (c *K8sResourceClient) SpecJSON(v *unstructured.UnstructuredList) string { + c.t.Helper() + + clean := []any{} + for _, item := range v.Items { + clean = append(clean, item.Object["spec"]) + } + + out, err := json.MarshalIndent(clean, "", " ") + require.NoError(c.t, err) + return string(out) +} + // remove the meta keys that are expected to change each time -func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string { +func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured, replaceMeta ...string) string { + c.t.Helper() + copy := c.sanitizeObject(v) + + out, err := json.MarshalIndent(copy, "", " ") + // fmt.Printf("%s", out) + require.NoError(c.t, err) + return string(out) +} + +// remove the meta keys that are expected to change each time +func (c *K8sResourceClient) sanitizeObject(v *unstructured.Unstructured, replaceMeta ...string) *unstructured.Unstructured { c.t.Helper() deep := v.DeepCopy() @@ -170,24 +209,24 @@ func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string { meta, ok := copy["metadata"].(map[string]any) require.True(c.t, ok) - replaceMeta := []string{"creationTimestamp", "resourceVersion", "uid"} + replaceMeta = append(replaceMeta, "creationTimestamp", "resourceVersion", "uid") for _, key := range replaceMeta { old, ok := meta[key] - require.True(c.t, ok) - require.NotEmpty(c.t, old) - meta[key] = fmt.Sprintf("${%s}", key) + if ok { + require.NotEmpty(c.t, old) + meta[key] = fmt.Sprintf("${%s}", key) + } } - - out, err := json.MarshalIndent(copy, "", " ") - // fmt.Printf("%s", out) - require.NoError(c.t, err) - return string(out) + return deep } type OrgUsers struct { Admin User Editor User Viewer User + + // The team with admin+editor in it (but not viewer) + Staff team.Team } type User struct { @@ -243,7 +282,7 @@ func (c *K8sTestHelper) PostResource(user User, resource string, payload AnyReso namespace := payload.Namespace if namespace == "" { - namespace = c.namespacer(user.Identity.GetOrgID()) + namespace = c.Namespacer(user.Identity.GetOrgID()) } path := fmt.Sprintf("/apis/%s/namespaces/%s/%s", @@ -383,11 +422,14 @@ func (c *K8sTestHelper) LoadYAMLOrJSON(body string) *unstructured.Unstructured { func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers { c.t.Helper() - return OrgUsers{ + users := OrgUsers{ Admin: c.CreateUser("admin", orgName, org.RoleAdmin, nil), Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil), Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil), } + users.Staff = c.CreateTeam("staff", "staff@"+orgName, users.Admin.Identity.GetOrgID()) + // TODO add admin and editor to staff + return users } func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.RoleType, permissions []resourcepermissions.SetResourcePermissionCommand) User { @@ -542,3 +584,11 @@ func (c *K8sTestHelper) CreateDS(cmd *datasources.AddDataSourceCommand) *datasou require.NoError(c.t, err) return dataSource } + +func (c *K8sTestHelper) CreateTeam(name, email string, orgID int64) team.Team { + c.t.Helper() + + team, err := c.env.Server.HTTPServer.TeamService.CreateTeam(context.Background(), name, email, orgID) + require.NoError(c.t, err) + return team +} diff --git a/pkg/tests/apis/identity/identity_test.go b/pkg/tests/apis/identity/identity_test.go new file mode 100644 index 00000000000..a98858e8ea3 --- /dev/null +++ b/pkg/tests/apis/identity/identity_test.go @@ -0,0 +1,150 @@ +package identity + +import ( + "context" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var gvrTeams = schema.GroupVersionResource{ + Group: "identity.grafana.app", + Version: "v0alpha1", + Resource: "teams", +} + +var gvrUsers = schema.GroupVersionResource{ + Group: "identity.grafana.app", + Version: "v0alpha1", + Resource: "users", +} + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationRequiresDevMode(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, // should fail + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service + }, + }) + + _, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("identity.grafana.app/v0alpha1") + require.Error(t, err) +} + +func TestIntegrationIdentity(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, // required for experimental APIs + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service + }, + }) + _, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("identity.grafana.app/v0alpha1") + require.NoError(t, err) + + t.Run("read only views", func(t *testing.T) { + ctx := context.Background() + teamClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrTeams, + }) + 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": [ + { + "apiVersion": "identity.grafana.app/v0alpha1", + "kind": "Team", + "metadata": { + "annotations": { + "grafana.app/originName": "SQL", + "grafana.app/originPath": "${originPath}" + }, + "creationTimestamp": "${creationTimestamp}", + "name": "${name}", + "namespace": "default", + "resourceVersion": "${resourceVersion}" + }, + "spec": { + "email": "staff@Org1", + "name": "staff" + } + } + ] + }`, found) + + // Org1 users + userClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrUsers, + }) + rsp, err = userClient.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + // Get just the specs (avoids values that change with each deployment) + found = teamClient.SpecJSON(rsp) + // fmt.Printf("%s", found) // NOTE the first value does not have an email or login + require.JSONEq(t, `[ + {}, + { + "email": "admin-1", + "login": "admin-1" + }, + { + "email": "editor-1", + "login": "editor-1" + }, + { + "email": "viewer-1", + "login": "viewer-1" + } + ]`, found) + + // OrgB users + userClient = helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, // super admin + Namespace: helper.Namespacer(helper.OrgB.Admin.Identity.GetOrgID()), // list values for orgB with super admin user + GVR: gvrUsers, + }) + rsp, err = userClient.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + // Get just the specs (avoids values that change with each deployment) + found = teamClient.SpecJSON(rsp) + fmt.Printf("%s", found) // NOTE the first value does not have an email or login + require.JSONEq(t, `[ + { + "email": "admin-3", + "login": "admin-3" + }, + { + "email": "editor-3", + "login": "editor-3" + }, + { + "email": "viewer-3", + "login": "viewer-3" + } + ]`, found) + }) +}