mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access Control: Add service accounts (#38994)
* Add extra fields to OSS types to support enterprise * Create a service account at the same time as the API key * Use service account credentials when accessing API with APIkey * Add GetRole to service, merge RoleDTO and Role structs This patch merges the identical OSS and Enterprise data structures, which improves the code for two reasons: 1. Makes switching between OSS and Enterprise easier 2. Reduces the chance of incompatibilities developing between the same functions in OSS and Enterprise * If API key is not linked to a service account, continue login as usual * Fallback to old auth if no service account linked to key * Add CloneUserToServiceAccount * Adding LinkAPIKeyToServiceAccount * Handle api key link error * Better error messages for OSS accesscontrol * Set an invalid user id as default * Re-arrange field names * ServiceAccountId is integer * Better error messages Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: Eric Leijonmarck <eric.leijonmarck@gmail.com> Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
@@ -72,6 +72,17 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext, cmd models.AddApiKeyComman
|
||||
}
|
||||
}
|
||||
cmd.OrgId = c.OrgId
|
||||
var err error
|
||||
var serviceAccount *models.User = &models.User{Id: -1}
|
||||
if hs.Cfg.FeatureToggles["service-accounts"] {
|
||||
if cmd.CreateNewServiceAccount {
|
||||
serviceAccount, err = hs.AccessControl.CloneUserToServiceAccount(c.Req.Context(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return response.Error(500, "Unable to clone user to service account", err)
|
||||
}
|
||||
cmd.ServiceAccountId = serviceAccount.Id
|
||||
}
|
||||
}
|
||||
|
||||
newKeyInfo, err := apikeygen.New(cmd.OrgId, cmd.Name)
|
||||
if err != nil {
|
||||
|
@@ -13,24 +13,27 @@ var (
|
||||
)
|
||||
|
||||
type ApiKey struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
Name string
|
||||
Key string
|
||||
Role RoleType
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Expires *int64
|
||||
Id int64
|
||||
OrgId int64
|
||||
Name string
|
||||
Key string
|
||||
Role RoleType
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Expires *int64
|
||||
ServiceAccountId int64
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
type AddApiKeyCommand struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Role RoleType `json:"role" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
Key string `json:"-"`
|
||||
SecondsToLive int64 `json:"secondsToLive"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Role RoleType `json:"role" binding:"Required"`
|
||||
OrgId int64 `json:"-"`
|
||||
Key string `json:"-"`
|
||||
SecondsToLive int64 `json:"secondsToLive"`
|
||||
ServiceAccountId int64 `json:"serviceAccount"`
|
||||
CreateNewServiceAccount bool `json:"createServiceAccount"`
|
||||
|
||||
Result *ApiKey `json:"-"`
|
||||
}
|
||||
|
@@ -56,18 +56,19 @@ func (u *User) NameOrFallback() string {
|
||||
// COMMANDS
|
||||
|
||||
type CreateUserCommand struct {
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Company string
|
||||
OrgId int64
|
||||
OrgName string
|
||||
Password string
|
||||
EmailVerified bool
|
||||
IsAdmin bool
|
||||
IsDisabled bool
|
||||
SkipOrgSetup bool
|
||||
DefaultOrgRole string
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
Company string
|
||||
OrgId int64
|
||||
OrgName string
|
||||
Password string
|
||||
EmailVerified bool
|
||||
IsAdmin bool
|
||||
IsDisabled bool
|
||||
SkipOrgSetup bool
|
||||
DefaultOrgRole string
|
||||
IsServiceAccount bool
|
||||
|
||||
Result User
|
||||
}
|
||||
|
@@ -14,6 +14,15 @@ type AccessControl interface {
|
||||
// GetUserPermissions returns user permissions.
|
||||
GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*Permission, error)
|
||||
|
||||
// GetUserRoles returns user roles.
|
||||
GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*RoleDTO, error)
|
||||
|
||||
// CloneUserToServiceAccount Creates a new service account and assigns it the same roles as the user has
|
||||
CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error)
|
||||
|
||||
// LinkAPIKeyToServiceAccount Connects an APIkey to a service account. Multiple API keys may be linked to one account.
|
||||
LinkAPIKeyToServiceAccount(ctx context.Context, ApiKey *models.ApiKey, serviceAccount *models.User) error
|
||||
|
||||
//IsDisabled returns if access control is enabled or not
|
||||
IsDisabled() bool
|
||||
|
||||
|
@@ -14,17 +14,24 @@ type fullAccessControl interface {
|
||||
}
|
||||
|
||||
type Calls struct {
|
||||
Evaluate []interface{}
|
||||
GetUserPermissions []interface{}
|
||||
IsDisabled []interface{}
|
||||
DeclareFixedRoles []interface{}
|
||||
GetUserBuiltInRoles []interface{}
|
||||
RegisterFixedRoles []interface{}
|
||||
CloneUserToServiceAccount []interface{}
|
||||
Evaluate []interface{}
|
||||
GetUserPermissions []interface{}
|
||||
GetUserRoles []interface{}
|
||||
IsDisabled []interface{}
|
||||
DeclareFixedRoles []interface{}
|
||||
GetUserBuiltInRoles []interface{}
|
||||
RegisterFixedRoles []interface{}
|
||||
LinkAPIKeyToServiceAccount []interface{}
|
||||
}
|
||||
|
||||
type Mock struct {
|
||||
// Unless an override is provided, user will be returned by CloneUserToServiceAccount
|
||||
createduser *models.User
|
||||
// Unless an override is provided, permissions will be returned by GetUserPermissions
|
||||
permissions []*accesscontrol.Permission
|
||||
// Unless an override is provided, roles will be returned by GetUserRoles
|
||||
roles []*accesscontrol.RoleDTO
|
||||
// Unless an override is provided, disabled will be returned by IsDisabled
|
||||
disabled bool
|
||||
// Unless an override is provided, builtInRoles will be returned by GetUserBuiltInRoles
|
||||
@@ -34,12 +41,15 @@ type Mock struct {
|
||||
Calls Calls
|
||||
|
||||
// Override functions
|
||||
EvaluateFunc func(context.Context, *models.SignedInUser, accesscontrol.Evaluator) (bool, error)
|
||||
GetUserPermissionsFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.Permission, error)
|
||||
IsDisabledFunc func() bool
|
||||
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
|
||||
GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string
|
||||
RegisterFixedRolesFunc func() error
|
||||
CloneUserToServiceAccountFunc func(context.Context, *models.SignedInUser) (*models.User, error)
|
||||
LinkAPIKeyToServiceAccountFunc func(context.Context, *models.ApiKey, *models.User) error
|
||||
EvaluateFunc func(context.Context, *models.SignedInUser, accesscontrol.Evaluator) (bool, error)
|
||||
GetUserPermissionsFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.Permission, error)
|
||||
GetUserRolesFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.RoleDTO, error)
|
||||
IsDisabledFunc func() bool
|
||||
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
|
||||
GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string
|
||||
RegisterFixedRolesFunc func() error
|
||||
}
|
||||
|
||||
// Ensure the mock stays in line with the interface
|
||||
@@ -99,6 +109,36 @@ func (m *Mock) GetUserPermissions(ctx context.Context, user *models.SignedInUser
|
||||
return m.permissions, nil
|
||||
}
|
||||
|
||||
func (m *Mock) GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.RoleDTO, error) {
|
||||
m.Calls.GetUserRoles = append(m.Calls.GetUserRoles, []interface{}{ctx, user})
|
||||
// Use override if provided
|
||||
if m.GetUserRolesFunc != nil {
|
||||
return m.GetUserRolesFunc(ctx, user)
|
||||
}
|
||||
// Otherwise return the Roles list
|
||||
return m.roles, nil
|
||||
}
|
||||
|
||||
func (m *Mock) CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error) {
|
||||
m.Calls.CloneUserToServiceAccount = append(m.Calls.CloneUserToServiceAccount, []interface{}{ctx, user})
|
||||
// Use override if provided
|
||||
if m.CloneUserToServiceAccountFunc != nil {
|
||||
return m.CloneUserToServiceAccountFunc(ctx, user)
|
||||
}
|
||||
// Otherwise return the user
|
||||
return m.createduser, nil
|
||||
}
|
||||
|
||||
func (m *Mock) LinkAPIKeyToServiceAccount(ctx context.Context, apikey *models.ApiKey, service_account *models.User) error {
|
||||
m.Calls.LinkAPIKeyToServiceAccount = append(m.Calls.LinkAPIKeyToServiceAccount, []interface{}{ctx, apikey, service_account})
|
||||
// Use override if provided
|
||||
if m.LinkAPIKeyToServiceAccountFunc != nil {
|
||||
return m.LinkAPIKeyToServiceAccountFunc(ctx, apikey, service_account)
|
||||
}
|
||||
// Otherwise return the default
|
||||
return nil
|
||||
}
|
||||
|
||||
// Middleware checks if service disabled or not to switch to fallback authorization.
|
||||
// This mock return m.disabled unless an override is provided.
|
||||
func (m *Mock) IsDisabled() bool {
|
||||
|
@@ -2,6 +2,7 @@ package ossaccesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
@@ -71,6 +72,21 @@ func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.Si
|
||||
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
|
||||
}
|
||||
|
||||
// GetUserRoles returns user permissions based on built-in roles
|
||||
func (ac *OSSAccessControlService) GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.RoleDTO, error) {
|
||||
return nil, errors.New("unsupported function") //OSS users will continue to use builtin roles via GetUserPermissions
|
||||
}
|
||||
|
||||
// CloneUserToServiceAccount creates a service account with permissions based on a user
|
||||
func (ac *OSSAccessControlService) CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error) {
|
||||
return nil, errors.New("clone user not implemented yet in service accounts") //Please switch on Enterprise to test this
|
||||
}
|
||||
|
||||
// Link creates a service account with permissions based on a user
|
||||
func (ac *OSSAccessControlService) LinkAPIKeyToServiceAccount(context.Context, *models.ApiKey, *models.User) error {
|
||||
return errors.New("link SA not implemented yet in service accounts") //Please switch on Enterprise to test this
|
||||
}
|
||||
|
||||
// GetUserPermissions returns user permissions based on built-in roles
|
||||
func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) {
|
||||
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
|
||||
|
@@ -218,11 +218,33 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
|
||||
return true
|
||||
}
|
||||
|
||||
if apikey.ServiceAccountId < 1 { //There is no service account attached to the apikey
|
||||
//Use the old APIkey method. This provides backwards compatibility.
|
||||
reqContext.SignedInUser = &models.SignedInUser{}
|
||||
reqContext.OrgRole = apikey.Role
|
||||
reqContext.ApiKeyId = apikey.Id
|
||||
reqContext.OrgId = apikey.OrgId
|
||||
reqContext.IsSignedIn = true
|
||||
return true
|
||||
}
|
||||
|
||||
//There is a service account attached to the API key
|
||||
|
||||
//Use service account linked to API key as the signed in user
|
||||
query := models.GetSignedInUserQuery{UserId: apikey.ServiceAccountId, OrgId: apikey.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
reqContext.Logger.Error(
|
||||
"Failed to link API key to service account in",
|
||||
"id", query.UserId,
|
||||
"org", query.OrgId,
|
||||
"err", err,
|
||||
)
|
||||
reqContext.JsonApiErr(500, "Unable to link API key to service account", err)
|
||||
return true
|
||||
}
|
||||
|
||||
reqContext.IsSignedIn = true
|
||||
reqContext.SignedInUser = &models.SignedInUser{}
|
||||
reqContext.OrgRole = apikey.Role
|
||||
reqContext.ApiKeyId = apikey.Id
|
||||
reqContext.OrgId = apikey.OrgId
|
||||
reqContext.SignedInUser = query.Result
|
||||
return true
|
||||
}
|
||||
|
||||
|
@@ -76,14 +76,16 @@ func AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) error {
|
||||
} else if cmd.SecondsToLive < 0 {
|
||||
return models.ErrInvalidApiKeyExpiration
|
||||
}
|
||||
|
||||
t := models.ApiKey{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Role: cmd.Role,
|
||||
Key: cmd.Key,
|
||||
Created: updated,
|
||||
Updated: updated,
|
||||
Expires: expires,
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Role: cmd.Role,
|
||||
Key: cmd.Key,
|
||||
Created: updated,
|
||||
Updated: updated,
|
||||
Expires: expires,
|
||||
ServiceAccountId: cmd.ServiceAccountId,
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(&t); err != nil {
|
||||
|
@@ -82,4 +82,8 @@ func addApiKeyMigrations(mg *Migrator) {
|
||||
mg.AddMigration("Add expires to api_key table", NewAddColumnMigration(apiKeyV2, &Column{
|
||||
Name: "expires", Type: DB_BigInt, Nullable: true,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Add service account foreign key", NewAddColumnMigration(apiKeyV2, &Column{
|
||||
Name: "service_account_id", Type: DB_BigInt, Nullable: true,
|
||||
}))
|
||||
}
|
||||
|
Reference in New Issue
Block a user