IAM: Create Service Account API and legacy store impl (#110411)
* wip * IAM: Create Service Account * Add dual writer * Update openapi_test.go * Add integration tests * Add sql tests * Add Role to SA spec, add validation, add DBTime, add tests * Format, update test * Fixes * Add check for External * Address feedback * Update tests * Address feedback * make gen-go * Simplify a bit * Fixes * make update-workspace * Update pkg/registry/apis/iam/serviceaccount/store.go Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * Address feedback, add test for generateName --------- Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
INSERT INTO {{ .Ident .UserTable }}
|
||||
(uid, version, login, email, name, org_id, is_admin, is_disabled, email_verified,
|
||||
is_provisioned, is_service_account, salt, rands, created, updated, last_seen_at)
|
||||
VALUES
|
||||
({{ .Arg .Command.UID }}, 0, {{ .Arg .Command.Login }}, {{ .Arg .Command.Email }}, {{ .Arg .Command.Name }},
|
||||
{{ .Arg .Command.OrgID }}, false, {{ .Arg .Command.IsDisabled }}, false,
|
||||
false, true, '', '', {{ .Arg .Command.Created }}, {{ .Arg .Command.Updated }}, {{ .Arg .Command.LastSeenAt }})
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
@@ -107,10 +108,28 @@ type ServiceAccount struct {
|
||||
UID string
|
||||
Name string
|
||||
Disabled bool
|
||||
Role string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
type CreateServiceAccountCommand struct {
|
||||
UID string
|
||||
Name string
|
||||
Email string
|
||||
Login string
|
||||
Role string
|
||||
IsDisabled bool
|
||||
OrgID int64
|
||||
Created DBTime
|
||||
Updated DBTime
|
||||
LastSeenAt time.Time
|
||||
}
|
||||
|
||||
type CreateServiceAccountResult struct {
|
||||
ServiceAccount ServiceAccount
|
||||
}
|
||||
|
||||
var sqlQueryServiceAccountsTemplate = mustTemplate("service_accounts_query.sql")
|
||||
|
||||
func newListServiceAccounts(sql *legacysql.LegacyDatabaseHelper, q *ListServiceAccountsQuery) listServiceAccountsQuery {
|
||||
@@ -167,7 +186,7 @@ func (s *legacySQLStore) ListServiceAccounts(ctx context.Context, ns claims.Name
|
||||
var lastID int64
|
||||
for rows.Next() {
|
||||
var s ServiceAccount
|
||||
err := rows.Scan(&s.ID, &s.UID, &s.Name, &s.Disabled, &s.Created, &s.Updated)
|
||||
err := rows.Scan(&s.ID, &s.UID, &s.Name, &s.Disabled, &s.Role, &s.Created, &s.Updated)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
@@ -286,3 +305,98 @@ func (s *legacySQLStore) ListServiceAccountTokens(ctx context.Context, ns claims
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
var sqlCreateServiceAccountTemplate = mustTemplate("create_service_account.sql")
|
||||
|
||||
func newCreateServiceAccount(sql *legacysql.LegacyDatabaseHelper, cmd *CreateServiceAccountCommand) createServiceAccountQuery {
|
||||
return createServiceAccountQuery{
|
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||
UserTable: sql.Table("user"),
|
||||
OrgUserTable: sql.Table("org_user"),
|
||||
Command: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
type createServiceAccountQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
UserTable string
|
||||
OrgUserTable string
|
||||
Command *CreateServiceAccountCommand
|
||||
}
|
||||
|
||||
func (r createServiceAccountQuery) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *legacySQLStore) CreateServiceAccount(ctx context.Context, ns claims.NamespaceInfo, cmd CreateServiceAccountCommand) (*CreateServiceAccountResult, error) {
|
||||
cmd.OrgID = ns.OrgID
|
||||
cmd.Email = cmd.Login
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
lastSeenAt := now.AddDate(-10, 0, 0) // Set last seen 10 years ago like in user service
|
||||
|
||||
cmd.Created = NewDBTime(now)
|
||||
cmd.Updated = NewDBTime(now)
|
||||
cmd.LastSeenAt = lastSeenAt
|
||||
|
||||
if ns.OrgID == 0 {
|
||||
return nil, fmt.Errorf("expected non zero org id")
|
||||
}
|
||||
|
||||
sql, err := s.sql(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := newCreateServiceAccount(sql, &cmd)
|
||||
|
||||
var createdSA ServiceAccount
|
||||
err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error {
|
||||
userQuery, err := sqltemplate.Execute(sqlCreateServiceAccountTemplate, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("execute service account template %q: %w", sqlCreateServiceAccountTemplate.Name(), err)
|
||||
}
|
||||
|
||||
serviceAccountID, err := st.ExecWithReturningId(ctx, userQuery, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create service account: %w", err)
|
||||
}
|
||||
|
||||
orgUserCmd := &CreateOrgUserCommand{
|
||||
OrgID: ns.OrgID,
|
||||
UserID: serviceAccountID,
|
||||
Role: cmd.Role,
|
||||
Created: cmd.Created,
|
||||
Updated: cmd.Updated,
|
||||
}
|
||||
orgUserReq := newCreateOrgUser(sql, orgUserCmd)
|
||||
|
||||
orgUserQuery, err := sqltemplate.Execute(sqlCreateOrgUserTemplate, orgUserReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("execute org_user template %q: %w", sqlCreateOrgUserTemplate.Name(), err)
|
||||
}
|
||||
|
||||
_, err = st.Exec(ctx, orgUserQuery, orgUserReq.GetArgs()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create org_user relationship: %w", err)
|
||||
}
|
||||
|
||||
createdSA = ServiceAccount{
|
||||
ID: serviceAccountID,
|
||||
UID: cmd.UID,
|
||||
Name: cmd.Name,
|
||||
Role: cmd.Role,
|
||||
Disabled: cmd.IsDisabled,
|
||||
Created: cmd.Created.Time,
|
||||
Updated: cmd.Updated.Time,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CreateServiceAccountResult{ServiceAccount: createdSA}, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM {{ .Ident .UserTable }} as u JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id
|
||||
|
||||
@@ -2,9 +2,11 @@ package legacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"embed"
|
||||
"fmt"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
@@ -22,6 +24,8 @@ type LegacyIdentityStore interface {
|
||||
|
||||
GetServiceAccountInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetServiceAccountInternalIDQuery) (*GetServiceAccountInternalIDResult, error)
|
||||
ListServiceAccounts(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountsQuery) (*ListServiceAccountResult, error)
|
||||
CreateServiceAccount(ctx context.Context, ns claims.NamespaceInfo, cmd CreateServiceAccountCommand) (*CreateServiceAccountResult, error)
|
||||
|
||||
ListServiceAccountTokens(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountTokenQuery) (*ListServiceAccountTokenResult, error)
|
||||
|
||||
GetTeamInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetTeamInternalIDQuery) (*GetTeamInternalIDResult, error)
|
||||
@@ -30,9 +34,7 @@ type LegacyIdentityStore interface {
|
||||
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)
|
||||
}
|
||||
|
||||
var (
|
||||
_ LegacyIdentityStore = (*legacySQLStore)(nil)
|
||||
)
|
||||
var _ LegacyIdentityStore = (*legacySQLStore)(nil)
|
||||
|
||||
func NewLegacySQLStores(sql legacysql.LegacyDatabaseProvider) LegacyIdentityStore {
|
||||
return &legacySQLStore{
|
||||
@@ -58,3 +60,47 @@ func mustTemplate(filename string) *template.Template {
|
||||
}
|
||||
panic(fmt.Sprintf("template file not found: %s", filename))
|
||||
}
|
||||
|
||||
type DBTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func NewDBTime(t time.Time) DBTime {
|
||||
return DBTime{Time: t}
|
||||
}
|
||||
|
||||
func (t DBTime) Value() (driver.Value, error) {
|
||||
if t.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return t.Format(time.DateTime), nil
|
||||
}
|
||||
|
||||
func (t *DBTime) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
t.Time = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
var parsedTime time.Time
|
||||
var err error
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
parsedTime, err = time.Parse(time.DateTime, string(v))
|
||||
case string:
|
||||
parsedTime, err = time.Parse(time.DateTime, v)
|
||||
case time.Time:
|
||||
parsedTime = v
|
||||
default:
|
||||
return fmt.Errorf("could not scan type %T into DBTime", value)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse time: %w", err)
|
||||
}
|
||||
|
||||
t.Time = parsedTime
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,6 +88,12 @@ func TestIdentityQueries(t *testing.T) {
|
||||
return &v
|
||||
}
|
||||
|
||||
createServiceAccounts := func(cmd *CreateServiceAccountCommand) sqltemplate.SQLTemplate {
|
||||
v := newCreateServiceAccount(nodb, cmd)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
listServiceAccountTokens := func(q *ListServiceAccountTokenQuery) sqltemplate.SQLTemplate {
|
||||
v := newListServiceAccountTokens(nodb, q)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
@@ -361,8 +367,8 @@ func TestIdentityQueries(t *testing.T) {
|
||||
OrgID: 1,
|
||||
UserID: 123,
|
||||
Role: "Viewer",
|
||||
Created: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
Updated: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -371,8 +377,8 @@ func TestIdentityQueries(t *testing.T) {
|
||||
OrgID: 2,
|
||||
UserID: 456,
|
||||
Role: "Admin",
|
||||
Created: time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC),
|
||||
Updated: time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC),
|
||||
Created: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
|
||||
Updated: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -391,9 +397,9 @@ func TestIdentityQueries(t *testing.T) {
|
||||
IsProvisioned: false,
|
||||
Salt: "randomsalt",
|
||||
Rands: "randomrands",
|
||||
Created: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
Updated: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
LastSeenAt: time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
LastSeenAt: NewDBTime(time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
Role: "Viewer",
|
||||
}),
|
||||
},
|
||||
@@ -411,13 +417,43 @@ func TestIdentityQueries(t *testing.T) {
|
||||
IsProvisioned: true,
|
||||
Salt: "adminsalt",
|
||||
Rands: "adminrands",
|
||||
Created: time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC),
|
||||
Updated: time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC),
|
||||
LastSeenAt: time.Date(2013, 2, 1, 10, 30, 0, 0, time.UTC),
|
||||
Created: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
|
||||
Updated: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
|
||||
LastSeenAt: NewDBTime(time.Date(2013, 2, 1, 10, 30, 0, 0, time.UTC)),
|
||||
Role: "Admin",
|
||||
}),
|
||||
},
|
||||
},
|
||||
sqlCreateServiceAccountTemplate: {
|
||||
{
|
||||
Name: "create_service_account_basic",
|
||||
Data: createServiceAccounts(&CreateServiceAccountCommand{
|
||||
UID: "abcdef",
|
||||
Name: "Service Account 1",
|
||||
Email: "sa-1-service-account-1",
|
||||
Login: "sa-1-service-account-1",
|
||||
IsDisabled: false,
|
||||
OrgID: 1,
|
||||
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
|
||||
LastSeenAt: time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "create_service_account_disabled",
|
||||
Data: createServiceAccounts(&CreateServiceAccountCommand{
|
||||
UID: "abcdef",
|
||||
Name: "Disabled Service Account",
|
||||
Email: "sa-2-disabled-service-account",
|
||||
Login: "sa-2-disabled-service-account",
|
||||
IsDisabled: true,
|
||||
OrgID: 2,
|
||||
Created: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
|
||||
Updated: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
|
||||
LastSeenAt: time.Date(2013, 2, 1, 10, 30, 0, 0, time.UTC),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pkg/registry/apis/iam/legacy/testdata/mysql--create_service_account-create_service_account_basic.sql
Vendored
Executable
+7
@@ -0,0 +1,7 @@
|
||||
INSERT INTO `grafana`.`user`
|
||||
(uid, version, login, email, name, org_id, is_admin, is_disabled, email_verified,
|
||||
is_provisioned, is_service_account, salt, rands, created, updated, last_seen_at)
|
||||
VALUES
|
||||
('abcdef', 0, 'sa-1-service-account-1', 'sa-1-service-account-1', 'Service Account 1',
|
||||
1, false, FALSE, false,
|
||||
false, true, '', '', '2023-01-01 12:00:00 +0000 UTC', '2023-01-01 12:00:00 +0000 UTC', '2013-01-01 12:00:00 +0000 UTC')
|
||||
Vendored
Executable
+7
@@ -0,0 +1,7 @@
|
||||
INSERT INTO `grafana`.`user`
|
||||
(uid, version, login, email, name, org_id, is_admin, is_disabled, email_verified,
|
||||
is_provisioned, is_service_account, salt, rands, created, updated, last_seen_at)
|
||||
VALUES
|
||||
('abcdef', 0, 'sa-2-disabled-service-account', 'sa-2-disabled-service-account', 'Disabled Service Account',
|
||||
2, false, TRUE, false,
|
||||
false, true, '', '', '2023-02-01 10:30:00 +0000 UTC', '2023-02-01 10:30:00 +0000 UTC', '2013-02-01 10:30:00 +0000 UTC')
|
||||
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id
|
||||
|
||||
Vendored
Executable
+7
@@ -0,0 +1,7 @@
|
||||
INSERT INTO "grafana"."user"
|
||||
(uid, version, login, email, name, org_id, is_admin, is_disabled, email_verified,
|
||||
is_provisioned, is_service_account, salt, rands, created, updated, last_seen_at)
|
||||
VALUES
|
||||
('abcdef', 0, 'sa-1-service-account-1', 'sa-1-service-account-1', 'Service Account 1',
|
||||
1, false, FALSE, false,
|
||||
false, true, '', '', '2023-01-01 12:00:00 +0000 UTC', '2023-01-01 12:00:00 +0000 UTC', '2013-01-01 12:00:00 +0000 UTC')
|
||||
Vendored
Executable
+7
@@ -0,0 +1,7 @@
|
||||
INSERT INTO "grafana"."user"
|
||||
(uid, version, login, email, name, org_id, is_admin, is_disabled, email_verified,
|
||||
is_provisioned, is_service_account, salt, rands, created, updated, last_seen_at)
|
||||
VALUES
|
||||
('abcdef', 0, 'sa-2-disabled-service-account', 'sa-2-disabled-service-account', 'Disabled Service Account',
|
||||
2, false, TRUE, false,
|
||||
false, true, '', '', '2023-02-01 10:30:00 +0000 UTC', '2023-02-01 10:30:00 +0000 UTC', '2013-02-01 10:30:00 +0000 UTC')
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
|
||||
|
||||
Vendored
Executable
+7
@@ -0,0 +1,7 @@
|
||||
INSERT INTO "grafana"."user"
|
||||
(uid, version, login, email, name, org_id, is_admin, is_disabled, email_verified,
|
||||
is_provisioned, is_service_account, salt, rands, created, updated, last_seen_at)
|
||||
VALUES
|
||||
('abcdef', 0, 'sa-1-service-account-1', 'sa-1-service-account-1', 'Service Account 1',
|
||||
1, false, FALSE, false,
|
||||
false, true, '', '', '2023-01-01 12:00:00 +0000 UTC', '2023-01-01 12:00:00 +0000 UTC', '2013-01-01 12:00:00 +0000 UTC')
|
||||
Vendored
Executable
+7
@@ -0,0 +1,7 @@
|
||||
INSERT INTO "grafana"."user"
|
||||
(uid, version, login, email, name, org_id, is_admin, is_disabled, email_verified,
|
||||
is_provisioned, is_service_account, salt, rands, created, updated, last_seen_at)
|
||||
VALUES
|
||||
('abcdef', 0, 'sa-2-disabled-service-account', 'sa-2-disabled-service-account', 'Disabled Service Account',
|
||||
2, false, TRUE, false,
|
||||
false, true, '', '', '2023-02-01 10:30:00 +0000 UTC', '2023-02-01 10:30:00 +0000 UTC', '2013-02-01 10:30:00 +0000 UTC')
|
||||
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ SELECT
|
||||
u.uid,
|
||||
u.name,
|
||||
u.is_disabled,
|
||||
o.role,
|
||||
u.created,
|
||||
u.updated
|
||||
FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id
|
||||
|
||||
@@ -303,9 +303,9 @@ type CreateUserCommand struct {
|
||||
IsProvisioned bool
|
||||
Salt string
|
||||
Rands string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
LastSeenAt time.Time
|
||||
Created DBTime
|
||||
Updated DBTime
|
||||
LastSeenAt DBTime
|
||||
Role string
|
||||
}
|
||||
|
||||
@@ -317,8 +317,8 @@ type CreateOrgUserCommand struct {
|
||||
OrgID int64
|
||||
UserID int64
|
||||
Role string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Created DBTime
|
||||
Updated DBTime
|
||||
}
|
||||
|
||||
type DeleteUserCommand struct {
|
||||
@@ -395,9 +395,9 @@ func (s *legacySQLStore) CreateUser(ctx context.Context, ns claims.NamespaceInfo
|
||||
|
||||
cmd.Salt = salt
|
||||
cmd.Rands = rands
|
||||
cmd.Created = now
|
||||
cmd.Updated = now
|
||||
cmd.LastSeenAt = lastSeenAt
|
||||
cmd.Created = NewDBTime(now)
|
||||
cmd.Updated = NewDBTime(now)
|
||||
cmd.LastSeenAt = NewDBTime(lastSeenAt)
|
||||
cmd.Role = "Viewer" // TODO: https://github.com/grafana/identity-access-team/issues/1552
|
||||
|
||||
sql, err := s.sql(ctx)
|
||||
@@ -451,9 +451,9 @@ func (s *legacySQLStore) CreateUser(ctx context.Context, ns claims.NamespaceInfo
|
||||
IsProvisioned: cmd.IsProvisioned,
|
||||
Salt: cmd.Salt,
|
||||
Rands: cmd.Rands,
|
||||
Created: cmd.Created,
|
||||
Updated: cmd.Updated,
|
||||
LastSeenAt: cmd.LastSeenAt,
|
||||
Created: cmd.Created.Time,
|
||||
Updated: cmd.Updated.Time,
|
||||
LastSeenAt: cmd.LastSeenAt.Time,
|
||||
IsServiceAccount: false,
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
teamBindingResource := iamv0.TeamBindingResourceInfo
|
||||
storage[teamBindingResource.StoragePath()] = team.NewLegacyBindingStore(b.store)
|
||||
|
||||
// User store registration
|
||||
userResource := iamv0.UserResourceInfo
|
||||
legacyStore := user.NewLegacyStore(b.store, b.legacyAccessClient, b.enableAuthnMutation)
|
||||
storage[userResource.StoragePath()] = legacyStore
|
||||
@@ -157,8 +158,26 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
|
||||
}
|
||||
|
||||
storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store)
|
||||
|
||||
// Service Accounts store registration
|
||||
serviceAccountResource := iamv0.ServiceAccountResourceInfo
|
||||
storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.legacyAccessClient)
|
||||
saLegacyStore := serviceaccount.NewLegacyStore(b.store, b.legacyAccessClient, b.enableAuthnMutation)
|
||||
storage[serviceAccountResource.StoragePath()] = saLegacyStore
|
||||
|
||||
if b.enableDualWriter {
|
||||
store, err := grafanaregistry.NewRegistryStore(opts.Scheme, serviceAccountResource, opts.OptsGetter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dw, err := opts.DualWriteBuilder(serviceAccountResource.GroupResource(), saLegacyStore, store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storage[serviceAccountResource.StoragePath()] = dw
|
||||
}
|
||||
|
||||
storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store)
|
||||
|
||||
if b.sso != nil {
|
||||
@@ -269,15 +288,21 @@ func (b *IdentityAccessManagementAPIBuilder) GetAuthorizer() authorizer.Authoriz
|
||||
func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
switch a.GetOperation() {
|
||||
case admission.Create:
|
||||
if a.GetKind() == iamv0.UserResourceInfo.GroupVersionKind() {
|
||||
switch typedObj := a.GetObject().(type) {
|
||||
case *iamv0.User:
|
||||
return b.validateCreateUser(ctx, a, o)
|
||||
case *iamv0.ServiceAccount:
|
||||
return serviceaccount.ValidateOnCreate(ctx, typedObj)
|
||||
}
|
||||
return nil
|
||||
case admission.Connect:
|
||||
case admission.Delete:
|
||||
case admission.Update:
|
||||
return nil
|
||||
case admission.Delete:
|
||||
return nil
|
||||
case admission.Connect:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -289,7 +314,7 @@ func (b *IdentityAccessManagementAPIBuilder) validateCreateUser(ctx context.Cont
|
||||
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return apierrors.NewBadRequest("no identity found")
|
||||
return apierrors.NewUnauthorized("no identity found")
|
||||
}
|
||||
|
||||
// Temporary validation that the user is not trying to create a Grafana Admin without being a Grafana Admin.
|
||||
@@ -312,8 +337,11 @@ func (b *IdentityAccessManagementAPIBuilder) validateCreateUser(ctx context.Cont
|
||||
func (b *IdentityAccessManagementAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
switch a.GetOperation() {
|
||||
case admission.Create:
|
||||
if a.GetKind() == iamv0.UserResourceInfo.GroupVersionKind() {
|
||||
return b.mutateUser(ctx, a, o)
|
||||
switch typedObj := a.GetObject().(type) {
|
||||
case *iamv0.User:
|
||||
return user.MutateOnCreate(ctx, typedObj)
|
||||
case *iamv0.ServiceAccount:
|
||||
return serviceaccount.MutateOnCreate(ctx, typedObj)
|
||||
}
|
||||
return nil
|
||||
case admission.Update:
|
||||
@@ -327,25 +355,6 @@ func (b *IdentityAccessManagementAPIBuilder) Mutate(ctx context.Context, a admis
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IdentityAccessManagementAPIBuilder) mutateUser(_ context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
userObj, ok := a.GetObject().(*iamv0.User)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
userObj.Spec.Email = strings.ToLower(userObj.Spec.Email)
|
||||
userObj.Spec.Login = strings.ToLower(userObj.Spec.Login)
|
||||
|
||||
if userObj.Spec.Login == "" {
|
||||
userObj.Spec.Login = userObj.Spec.Email
|
||||
}
|
||||
if userObj.Spec.Email == "" {
|
||||
userObj.Spec.Email = userObj.Spec.Login
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLocalStore(resourceInfo utils.ResourceInfo, scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter,
|
||||
reg prometheus.Registerer, ac types.AccessClient, storageBackend resource.StorageBackend) (grafanarest.Storage, error) {
|
||||
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
)
|
||||
|
||||
func MutateOnCreate(ctx context.Context, obj *iamv0alpha1.ServiceAccount) error {
|
||||
// External service accounts have None org role by default
|
||||
if obj.Spec.Plugin != "" && obj.Spec.Role == "" {
|
||||
obj.Spec.Role = iamv0alpha1.ServiceAccountOrgRoleNone
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
func TestMutateOnCreate(t *testing.T) {
|
||||
ctx := request.WithNamespace(context.Background(), "default")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputSA *iamv0alpha1.ServiceAccount
|
||||
expectedRole iamv0alpha1.ServiceAccountOrgRole
|
||||
}{
|
||||
{
|
||||
name: "non-external sa with editor role",
|
||||
inputSA: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "My Test SA",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleEditor,
|
||||
},
|
||||
},
|
||||
expectedRole: iamv0alpha1.ServiceAccountOrgRoleEditor,
|
||||
},
|
||||
{
|
||||
name: "external sa with admin role is not overridden",
|
||||
inputSA: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "grafana-plugin-name",
|
||||
Plugin: "grafana-plugin-name",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleAdmin,
|
||||
},
|
||||
},
|
||||
expectedRole: iamv0alpha1.ServiceAccountOrgRoleAdmin,
|
||||
},
|
||||
{
|
||||
name: "external sa with no role specified gets none",
|
||||
inputSA: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "sa-1-extsvc-grafana-plugin-name",
|
||||
Plugin: "grafana-plugin-name",
|
||||
},
|
||||
},
|
||||
expectedRole: iamv0alpha1.ServiceAccountOrgRoleNone,
|
||||
},
|
||||
{
|
||||
name: "non-external sa with no role specified",
|
||||
inputSA: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "Another SA",
|
||||
},
|
||||
},
|
||||
expectedRole: "", // Role is not mutated if not present and not external
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := MutateOnCreate(ctx, tc.inputSA)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedRole, tc.inputSA.Spec.Role)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package serviceaccount
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -12,9 +14,12 @@ import (
|
||||
claims "github.com/grafana/authlib/types"
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,17 +28,85 @@ var (
|
||||
_ rest.Getter = (*LegacyStore)(nil)
|
||||
_ rest.Lister = (*LegacyStore)(nil)
|
||||
_ rest.Storage = (*LegacyStore)(nil)
|
||||
_ rest.CreaterUpdater = (*LegacyStore)(nil)
|
||||
_ rest.GracefulDeleter = (*LegacyStore)(nil)
|
||||
_ rest.CollectionDeleter = (*LegacyStore)(nil)
|
||||
)
|
||||
|
||||
var resource = iamv0alpha1.ServiceAccountResourceInfo
|
||||
|
||||
func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient) *LegacyStore {
|
||||
return &LegacyStore{store, ac}
|
||||
func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore {
|
||||
return &LegacyStore{store, ac, enableAuthnMutation}
|
||||
}
|
||||
|
||||
type LegacyStore struct {
|
||||
store legacy.LegacyIdentityStore
|
||||
ac claims.AccessClient
|
||||
store legacy.LegacyIdentityStore
|
||||
ac claims.AccessClient
|
||||
enableAuthnMutation bool
|
||||
}
|
||||
|
||||
// DeleteCollection implements rest.CollectionDeleter.
|
||||
func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete")
|
||||
}
|
||||
|
||||
// Delete implements rest.GracefulDeleter.
|
||||
func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete")
|
||||
}
|
||||
|
||||
// Update implements rest.Updater.
|
||||
func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update")
|
||||
}
|
||||
|
||||
// Create implements rest.Creater.
|
||||
func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
if !s.enableAuthnMutation {
|
||||
return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create")
|
||||
}
|
||||
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saObj, ok := obj.(*iamv0alpha1.ServiceAccount)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected ServiceAccount object, got %T", obj)
|
||||
}
|
||||
|
||||
if saObj.GenerateName != "" {
|
||||
saObj.Name = saObj.GenerateName + util.GenerateShortUID()
|
||||
saObj.GenerateName = ""
|
||||
}
|
||||
|
||||
if createValidation != nil {
|
||||
if err := createValidation(ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
login := serviceaccounts.GenerateLogin(serviceaccounts.ServiceAccountPrefix, ns.OrgID, saObj.Spec.Title)
|
||||
if saObj.Spec.Plugin != "" {
|
||||
login = serviceaccounts.ExtSvcLoginPrefix(ns.OrgID) + slugify.Slugify(saObj.Spec.Title)
|
||||
}
|
||||
|
||||
createCmd := legacy.CreateServiceAccountCommand{
|
||||
IsDisabled: saObj.Spec.Disabled,
|
||||
Name: saObj.Spec.Title,
|
||||
UID: saObj.Name,
|
||||
Login: strings.ToLower(login),
|
||||
Role: string(saObj.Spec.Role),
|
||||
}
|
||||
|
||||
result, err := s.store.CreateServiceAccount(ctx, ns, createCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iamSA := s.toSAItem(result.ServiceAccount, ns.Value)
|
||||
return &iamSA, nil
|
||||
}
|
||||
|
||||
func (s *LegacyStore) New() runtime.Object {
|
||||
@@ -72,7 +145,7 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
|
||||
|
||||
items := make([]iamv0alpha1.ServiceAccount, 0, len(found.Items))
|
||||
for _, sa := range found.Items {
|
||||
items = append(items, toSAItem(sa, ns.Value))
|
||||
items = append(items, s.toSAItem(sa, ns.Value))
|
||||
}
|
||||
|
||||
return &common.ListResponse[iamv0alpha1.ServiceAccount]{
|
||||
@@ -93,7 +166,7 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func toSAItem(sa legacy.ServiceAccount, ns string) iamv0alpha1.ServiceAccount {
|
||||
func (s *LegacyStore) toSAItem(sa legacy.ServiceAccount, ns string) iamv0alpha1.ServiceAccount {
|
||||
item := iamv0alpha1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: sa.UID,
|
||||
@@ -102,8 +175,10 @@ func toSAItem(sa legacy.ServiceAccount, ns string) iamv0alpha1.ServiceAccount {
|
||||
CreationTimestamp: metav1.NewTime(sa.Created),
|
||||
},
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Plugin: extractPluginNameFromTitle(sa.Name),
|
||||
Title: sa.Name,
|
||||
Disabled: sa.Disabled,
|
||||
Role: iamv0alpha1.ServiceAccountOrgRole(sa.Role),
|
||||
},
|
||||
}
|
||||
obj, _ := utils.MetaAccessor(&item)
|
||||
@@ -112,6 +187,13 @@ func toSAItem(sa legacy.ServiceAccount, ns string) iamv0alpha1.ServiceAccount {
|
||||
return item
|
||||
}
|
||||
|
||||
func extractPluginNameFromTitle(title string) string {
|
||||
if strings.HasPrefix(title, serviceaccounts.ExtSvcPrefix) {
|
||||
return strings.TrimLeft(title, serviceaccounts.ExtSvcPrefix)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
ns, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
@@ -130,6 +212,6 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO
|
||||
return nil, resource.NewNotFound(name)
|
||||
}
|
||||
|
||||
res := toSAItem(found.Items[0], ns.Value)
|
||||
res := s.toSAItem(found.Items[0], ns.Value)
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
)
|
||||
|
||||
func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.ServiceAccount) error {
|
||||
if obj.Spec.Title == "" {
|
||||
return apierrors.NewBadRequest("service account must have a title")
|
||||
}
|
||||
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return apierrors.NewUnauthorized("no identity found")
|
||||
}
|
||||
|
||||
requestedRole := identity.RoleType(obj.Spec.Role)
|
||||
if !requestedRole.IsValid() {
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("invalid role: %s", requestedRole))
|
||||
}
|
||||
|
||||
if obj.Spec.Plugin != "" {
|
||||
if !strings.HasPrefix(obj.Spec.Title, serviceaccounts.ExtSvcPrefix) {
|
||||
return apierrors.NewBadRequest("title of external service accounts must start with " + serviceaccounts.ExtSvcPrefix)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(obj.Spec.Title, strings.ToLower(obj.Spec.Plugin)) {
|
||||
return apierrors.NewBadRequest("title of external service accounts must end with " + strings.ToLower(obj.Spec.Plugin))
|
||||
}
|
||||
|
||||
if !requester.IsIdentityType(types.TypeAccessPolicy) {
|
||||
return apierrors.NewForbidden(iamv0alpha1.ServiceAccountResourceInfo.GroupResource(),
|
||||
obj.Name,
|
||||
fmt.Errorf("only service identities can create external service accounts"))
|
||||
}
|
||||
|
||||
if obj.Spec.Role != iamv0alpha1.ServiceAccountOrgRoleNone {
|
||||
return apierrors.NewBadRequest("external service accounts must have role None")
|
||||
}
|
||||
}
|
||||
|
||||
if !requester.HasRole(requestedRole) {
|
||||
return apierrors.NewForbidden(iamv0alpha1.ServiceAccountResourceInfo.GroupResource(),
|
||||
obj.Name,
|
||||
fmt.Errorf("can not assign a role higher than user's role"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
)
|
||||
|
||||
func TestValidateOnCreate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serviceAccount *iamv0alpha1.ServiceAccount
|
||||
requester *identity.StaticRequester
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "valid service account with user requester",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "Test Service Account",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleViewer,
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty title",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleViewer,
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "service account must have a title",
|
||||
},
|
||||
{
|
||||
name: "invalid role",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "Test Service Account",
|
||||
Role: "InvalidRole",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "invalid role",
|
||||
},
|
||||
{
|
||||
name: "role higher than requester's role",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "Test Service Account",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleAdmin,
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleViewer,
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "can not assign a role higher than user's role",
|
||||
},
|
||||
{
|
||||
name: "external service account - valid",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: serviceaccounts.ExtSvcPrefix + "test-plugin",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleNone,
|
||||
Plugin: "test-plugin",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeAccessPolicy,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "external service account - invalid title prefix",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "invalid-prefix-test",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleNone,
|
||||
Plugin: "test",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeAccessPolicy,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "title of external service accounts must start with " + serviceaccounts.ExtSvcPrefix,
|
||||
},
|
||||
{
|
||||
name: "external service account - invalid title suffix",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: serviceaccounts.ExtSvcPrefix + "wrong-suffix",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleNone,
|
||||
Plugin: "test",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeAccessPolicy,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "title of external service accounts must end with test",
|
||||
},
|
||||
{
|
||||
name: "external service account - non-access-policy requester",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: serviceaccounts.ExtSvcPrefix + "test-test",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleNone,
|
||||
Plugin: "test",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeUser,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "only service identities can create external service accounts",
|
||||
},
|
||||
{
|
||||
name: "external service account - role not None",
|
||||
serviceAccount: &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: serviceaccounts.ExtSvcPrefix + "test-test",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleViewer,
|
||||
Plugin: "test",
|
||||
},
|
||||
},
|
||||
requester: &identity.StaticRequester{
|
||||
Type: types.TypeAccessPolicy,
|
||||
OrgRole: identity.RoleAdmin,
|
||||
},
|
||||
expectError: true,
|
||||
errorContains: "external service accounts must have role None",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := identity.WithRequester(
|
||||
context.Background(),
|
||||
tt.requester,
|
||||
)
|
||||
|
||||
err := ValidateOnCreate(ctx, tt.serviceAccount)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
if tt.errorContains != "" {
|
||||
require.Contains(t, err.Error(), tt.errorContains)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOnCreate_NoRequester(t *testing.T) {
|
||||
serviceAccount := &iamv0alpha1.ServiceAccount{
|
||||
Spec: iamv0alpha1.ServiceAccountSpec{
|
||||
Title: "Test Service Account",
|
||||
Role: iamv0alpha1.ServiceAccountOrgRoleViewer,
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateOnCreate(context.Background(), serviceAccount)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no identity found")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
)
|
||||
|
||||
func MutateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
|
||||
obj.Spec.Email = strings.ToLower(obj.Spec.Email)
|
||||
obj.Spec.Login = strings.ToLower(obj.Spec.Login)
|
||||
|
||||
if obj.Spec.Login == "" {
|
||||
obj.Spec.Login = obj.Spec.Email
|
||||
}
|
||||
if obj.Spec.Email == "" {
|
||||
obj.Spec.Email = obj.Spec.Login
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMutateOnCreate_LoginEmail(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputUser *iamv0alpha1.User
|
||||
expectedLogin string
|
||||
expectedEmail string
|
||||
}{
|
||||
{
|
||||
name: "login and email provided with mixed case",
|
||||
inputUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Login: "Test.User",
|
||||
Email: "Test.User@example.com",
|
||||
},
|
||||
},
|
||||
expectedLogin: "test.user",
|
||||
expectedEmail: "test.user@example.com",
|
||||
},
|
||||
{
|
||||
name: "only email provided",
|
||||
inputUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Email: "Only.Email@example.com",
|
||||
},
|
||||
},
|
||||
expectedLogin: "only.email@example.com",
|
||||
expectedEmail: "only.email@example.com",
|
||||
},
|
||||
{
|
||||
name: "only login provided",
|
||||
inputUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Login: "Only.Login",
|
||||
},
|
||||
},
|
||||
expectedLogin: "only.login",
|
||||
expectedEmail: "only.login",
|
||||
},
|
||||
{
|
||||
name: "login and email already lowercase",
|
||||
inputUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{
|
||||
Login: "already.lower",
|
||||
Email: "already.lower@example.com",
|
||||
},
|
||||
},
|
||||
expectedLogin: "already.lower",
|
||||
expectedEmail: "already.lower@example.com",
|
||||
},
|
||||
{
|
||||
name: "both login and email are empty",
|
||||
inputUser: &iamv0alpha1.User{
|
||||
Spec: iamv0alpha1.UserSpec{},
|
||||
},
|
||||
expectedLogin: "",
|
||||
expectedEmail: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := MutateOnCreate(context.Background(), tc.inputUser)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedLogin, tc.inputUser.Spec.Login)
|
||||
require.Equal(t, tc.expectedEmail, tc.inputUser.Spec.Email)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user