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:
Jeremy Price
2021-10-20 14:36:11 +02:00
committed by GitHub
parent bd97c79454
commit 6dbb6408d4
9 changed files with 156 additions and 48 deletions

View File

@@ -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 {

View File

@@ -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:"-"`
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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,
}))
}