mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Identity: Add endpoint to get display info for an identifier (#91828)
This commit is contained in:
parent
c7fdf8ce70
commit
a0cd89860e
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) error {
|
||||
&ServiceAccountList{},
|
||||
&Team{},
|
||||
&TeamList{},
|
||||
&IdentityDisplayList{},
|
||||
&IdentityDisplayResults{},
|
||||
)
|
||||
// metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
191
pkg/registry/apis/identity/display.go
Normal file
191
pkg/registry/apis/identity/display.go
Normal file
@ -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
|
||||
}
|
186
pkg/registry/apis/identity/legacy/legacy_sql.go
Normal file
186
pkg/registry/apis/identity/legacy/legacy_sql.go
Normal file
@ -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)
|
||||
}
|
58
pkg/registry/apis/identity/legacy/queries.go
Normal file
58
pkg/registry/apis/identity/legacy/queries.go
Normal file
@ -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
|
||||
}
|
207
pkg/registry/apis/identity/legacy/queries_test.go
Normal file
207
pkg/registry/apis/identity/legacy/queries_test.go
Normal file
@ -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, "<empty>")
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
13
pkg/registry/apis/identity/legacy/query_display.sql
Normal file
13
pkg/registry/apis/identity/legacy/query_display.sql
Normal file
@ -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
|
11
pkg/registry/apis/identity/legacy/query_teams.sql
Normal file
11
pkg/registry/apis/identity/legacy/query_teams.sql
Normal file
@ -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 }}
|
13
pkg/registry/apis/identity/legacy/query_users.sql
Normal file
13
pkg/registry/apis/identity/legacy/query_users.sql
Normal file
@ -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 }}
|
8
pkg/registry/apis/identity/legacy/testdata/mysql__display_ids.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/mysql__display_ids.sql
vendored
Executable file
@ -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
|
9
pkg/registry/apis/identity/legacy/testdata/mysql__display_ids_uids.sql
vendored
Executable file
9
pkg/registry/apis/identity/legacy/testdata/mysql__display_ids_uids.sql
vendored
Executable file
@ -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
|
8
pkg/registry/apis/identity/legacy/testdata/mysql__display_uids.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/mysql__display_uids.sql
vendored
Executable file
@ -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
|
5
pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_1.sql
vendored
Executable file
5
pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_1.sql
vendored
Executable file
@ -0,0 +1,5 @@
|
||||
SELECT id, uid, name, email, created, updated
|
||||
FROM "team"
|
||||
WHERE org_id = ?
|
||||
ORDER BY id asc
|
||||
LIMIT ?
|
6
pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_2.sql
vendored
Executable file
6
pkg/registry/apis/identity/legacy/testdata/mysql__teams_page_2.sql
vendored
Executable file
@ -0,0 +1,6 @@
|
||||
SELECT id, uid, name, email, created, updated
|
||||
FROM "team"
|
||||
WHERE org_id = ?
|
||||
AND id > ?
|
||||
ORDER BY id asc
|
||||
LIMIT ?
|
6
pkg/registry/apis/identity/legacy/testdata/mysql__teams_uid.sql
vendored
Executable file
6
pkg/registry/apis/identity/legacy/testdata/mysql__teams_uid.sql
vendored
Executable file
@ -0,0 +1,6 @@
|
||||
SELECT id, uid, name, email, created, updated
|
||||
FROM "team"
|
||||
WHERE org_id = ?
|
||||
AND uid = ?
|
||||
ORDER BY id asc
|
||||
LIMIT ?
|
7
pkg/registry/apis/identity/legacy/testdata/mysql__users_page_1.sql
vendored
Executable file
7
pkg/registry/apis/identity/legacy/testdata/mysql__users_page_1.sql
vendored
Executable file
@ -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 ?
|
8
pkg/registry/apis/identity/legacy/testdata/mysql__users_page_2.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/mysql__users_page_2.sql
vendored
Executable file
@ -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 ?
|
8
pkg/registry/apis/identity/legacy/testdata/mysql__users_uid.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/mysql__users_uid.sql
vendored
Executable file
@ -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 ?
|
8
pkg/registry/apis/identity/legacy/testdata/postgres__display_ids.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/postgres__display_ids.sql
vendored
Executable file
@ -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
|
9
pkg/registry/apis/identity/legacy/testdata/postgres__display_ids_uids.sql
vendored
Executable file
9
pkg/registry/apis/identity/legacy/testdata/postgres__display_ids_uids.sql
vendored
Executable file
@ -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
|
8
pkg/registry/apis/identity/legacy/testdata/postgres__display_uids.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/postgres__display_uids.sql
vendored
Executable file
@ -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
|
5
pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_1.sql
vendored
Executable file
5
pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_1.sql
vendored
Executable file
@ -0,0 +1,5 @@
|
||||
SELECT id, uid, name, email, created, updated
|
||||
FROM "team"
|
||||
WHERE org_id = $1
|
||||
ORDER BY id asc
|
||||
LIMIT $2
|
6
pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_2.sql
vendored
Executable file
6
pkg/registry/apis/identity/legacy/testdata/postgres__teams_page_2.sql
vendored
Executable file
@ -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
|
6
pkg/registry/apis/identity/legacy/testdata/postgres__teams_uid.sql
vendored
Executable file
6
pkg/registry/apis/identity/legacy/testdata/postgres__teams_uid.sql
vendored
Executable file
@ -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
|
7
pkg/registry/apis/identity/legacy/testdata/postgres__users_page_1.sql
vendored
Executable file
7
pkg/registry/apis/identity/legacy/testdata/postgres__users_page_1.sql
vendored
Executable file
@ -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
|
8
pkg/registry/apis/identity/legacy/testdata/postgres__users_page_2.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/postgres__users_page_2.sql
vendored
Executable file
@ -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
|
8
pkg/registry/apis/identity/legacy/testdata/postgres__users_uid.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/postgres__users_uid.sql
vendored
Executable file
@ -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
|
8
pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids.sql
vendored
Executable file
@ -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
|
9
pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids_uids.sql
vendored
Executable file
9
pkg/registry/apis/identity/legacy/testdata/sqlite__display_ids_uids.sql
vendored
Executable file
@ -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
|
8
pkg/registry/apis/identity/legacy/testdata/sqlite__display_uids.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/sqlite__display_uids.sql
vendored
Executable file
@ -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
|
5
pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_1.sql
vendored
Executable file
5
pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_1.sql
vendored
Executable file
@ -0,0 +1,5 @@
|
||||
SELECT id, uid, name, email, created, updated
|
||||
FROM "team"
|
||||
WHERE org_id = ?
|
||||
ORDER BY id asc
|
||||
LIMIT ?
|
6
pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_2.sql
vendored
Executable file
6
pkg/registry/apis/identity/legacy/testdata/sqlite__teams_page_2.sql
vendored
Executable file
@ -0,0 +1,6 @@
|
||||
SELECT id, uid, name, email, created, updated
|
||||
FROM "team"
|
||||
WHERE org_id = ?
|
||||
AND id > ?
|
||||
ORDER BY id asc
|
||||
LIMIT ?
|
6
pkg/registry/apis/identity/legacy/testdata/sqlite__teams_uid.sql
vendored
Executable file
6
pkg/registry/apis/identity/legacy/testdata/sqlite__teams_uid.sql
vendored
Executable file
@ -0,0 +1,6 @@
|
||||
SELECT id, uid, name, email, created, updated
|
||||
FROM "team"
|
||||
WHERE org_id = ?
|
||||
AND uid = ?
|
||||
ORDER BY id asc
|
||||
LIMIT ?
|
7
pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_1.sql
vendored
Executable file
7
pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_1.sql
vendored
Executable file
@ -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 ?
|
8
pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_2.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/sqlite__users_page_2.sql
vendored
Executable file
@ -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 ?
|
8
pkg/registry/apis/identity/legacy/testdata/sqlite__users_uid.sql
vendored
Executable file
8
pkg/registry/apis/identity/legacy/testdata/sqlite__users_uid.sql
vendored
Executable file
@ -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 ?
|
50
pkg/registry/apis/identity/legacy/types.go
Normal file
50
pkg/registry/apis/identity/legacy/types.go
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
87
pkg/registry/apis/identity/legacy_user_teams.go
Normal file
87
pkg/registry/apis/identity/legacy_user_teams.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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"`
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
8
pkg/storage/legacysql/README.md
Normal file
8
pkg/storage/legacysql/README.md
Normal file
@ -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.
|
51
pkg/storage/legacysql/rv.go
Normal file
51
pkg/storage/legacysql/rv.go
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
150
pkg/tests/apis/identity/identity_test.go
Normal file
150
pkg/tests/apis/identity/identity_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user