mirror of https://github.com/grafana/grafana
Identity: Add endpoint to get display info for an identifier (#91828)
parent
c7fdf8ce70
commit
a0cd89860e
@ -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 |
||||
|
||||
@ -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 |
||||
} |
||||
@ -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) |
||||
} |
||||
@ -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
|
||||
} |
||||
@ -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) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -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 |
||||
@ -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 }} |
||||
@ -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 }} |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -0,0 +1,5 @@ |
||||
SELECT id, uid, name, email, created, updated |
||||
FROM "team" |
||||
WHERE org_id = ? |
||||
ORDER BY id asc |
||||
LIMIT ? |
||||
@ -0,0 +1,6 @@ |
||||
SELECT id, uid, name, email, created, updated |
||||
FROM "team" |
||||
WHERE org_id = ? |
||||
AND id > ? |
||||
ORDER BY id asc |
||||
LIMIT ? |
||||
@ -0,0 +1,6 @@ |
||||
SELECT id, uid, name, email, created, updated |
||||
FROM "team" |
||||
WHERE org_id = ? |
||||
AND uid = ? |
||||
ORDER BY id asc |
||||
LIMIT ? |
||||
@ -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 ? |
||||
@ -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 ? |
||||
@ -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 ? |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -0,0 +1,5 @@ |
||||
SELECT id, uid, name, email, created, updated |
||||
FROM "team" |
||||
WHERE org_id = $1 |
||||
ORDER BY id asc |
||||
LIMIT $2 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -0,0 +1,5 @@ |
||||
SELECT id, uid, name, email, created, updated |
||||
FROM "team" |
||||
WHERE org_id = ? |
||||
ORDER BY id asc |
||||
LIMIT ? |
||||
@ -0,0 +1,6 @@ |
||||
SELECT id, uid, name, email, created, updated |
||||
FROM "team" |
||||
WHERE org_id = ? |
||||
AND id > ? |
||||
ORDER BY id asc |
||||
LIMIT ? |
||||
@ -0,0 +1,6 @@ |
||||
SELECT id, uid, name, email, created, updated |
||||
FROM "team" |
||||
WHERE org_id = ? |
||||
AND uid = ? |
||||
ORDER BY id asc |
||||
LIMIT ? |
||||
@ -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 ? |
||||
@ -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 ? |
||||
@ -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 ? |
||||
@ -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) |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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. |
||||
@ -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 |
||||
} |
||||
} |
||||
@ -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