auth: add serviceaccount proxy (#76815)

* Add proxy service template

* Replace SA srv with proxy for external SA srv

* Move service account prefix to a constant

* Prevent deletion from external service account

* Make SA validation a resusable function

* Add protection for creating service accounts

* Add protection when updating service accounts

* Add IsExternal field for service account

* Protect ext service account token generation

* Add verbose errors for form name or sa name

* add tests

* Add logs

* Adjusts tests

---------

Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
linoman
2023-10-23 14:09:42 +02:00
committed by GitHub
parent f6a0f6912f
commit 359d84799e
7 changed files with 400 additions and 16 deletions

View File

@@ -3,15 +3,23 @@ package extsvcaccounts
import (
"github.com/grafana/grafana/pkg/models/roletype"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/util/errutil"
)
const (
extsvcPrefix = "extsvc-"
ExtSvcPrefix = "extsvc-"
kvStoreType = "extsvc-token"
// #nosec G101 - this is not a hardcoded secret
tokenNamePrefix = "extsvc-token"
)
var (
ErrCannotBeDeleted = errutil.BadRequest("extsvcaccounts.ErrCannotBeDeleted", errutil.WithPublicMessage("external service account cannot be deleted"))
ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'"))
ErrCannotBeUpdated = errutil.BadRequest("extsvcaccounts.ErrCannotBeUpdated", errutil.WithPublicMessage("external service account cannot be updated"))
ErrCannotCreateToken = errutil.BadRequest("extsvcaccounts.ErrCannotCreateToken", errutil.WithPublicMessage("cannot add external service account token"))
)
// Credentials represents the credentials associated to an external service
type Credentials struct {
Secret string

View File

@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
)
type ExtSvcAccountsService struct {
@@ -23,7 +24,7 @@ type ExtSvcAccountsService struct {
skvStore kvstore.SecretsKVStore
}
func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc sa.Service, db db.DB, secretsSvc secrets.Service) *ExtSvcAccountsService {
func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc *manager.ServiceAccountsService, db db.DB, secretsSvc secrets.Service) *ExtSvcAccountsService {
logger := log.New("serviceauth.extsvcaccounts")
return &ExtSvcAccountsService{
acSvc: acSvc,
@@ -96,7 +97,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *
return 0, nil
}
saID, errRetrieve := esa.saSvc.RetrieveServiceAccountIdByName(ctx, cmd.OrgID, extsvcPrefix+cmd.ExtSvcSlug)
saID, errRetrieve := esa.saSvc.RetrieveServiceAccountIdByName(ctx, cmd.OrgID, ExtSvcPrefix+cmd.ExtSvcSlug)
if errRetrieve != nil && !errors.Is(errRetrieve, sa.ErrServiceAccountNotFound) {
return 0, errRetrieve
}
@@ -139,7 +140,7 @@ func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *sa
// Create a service account
esa.logger.Debug("Create service account", "service", cmd.ExtSvcSlug, "orgID", cmd.OrgID)
sa, err := esa.saSvc.CreateServiceAccount(ctx, cmd.OrgID, &sa.CreateServiceAccountForm{
Name: extsvcPrefix + cmd.ExtSvcSlug,
Name: ExtSvcPrefix + cmd.ExtSvcSlug,
Role: newRole(roletype.RoleNone),
IsDisabled: newBool(false),
})

View File

@@ -87,7 +87,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }),
mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID }))
@@ -114,7 +114,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }),
mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID }))
@@ -143,11 +143,11 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.SaSvc.AssertCalled(t, "CreateServiceAccount", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }),
mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool {
return cmd.Name == extsvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone
return cmd.Name == ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone
}),
)
env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything,
@@ -177,7 +177,7 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == extSvcOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything,
mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool {
return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug &&
@@ -257,7 +257,7 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }),
mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID }))
@@ -287,7 +287,7 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.SaSvc.AssertCalled(t, "DeleteServiceAccount", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }),
mock.MatchedBy(func(saID int64) bool { return saID == extSvcAccID }))
@@ -319,11 +319,11 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.SaSvc.AssertCalled(t, "CreateServiceAccount", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }),
mock.MatchedBy(func(cmd *sa.CreateServiceAccountForm) bool {
return cmd.Name == extsvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone
return cmd.Name == ExtSvcPrefix+extSvcSlug && *cmd.Role == roletype.RoleNone
}),
)
env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything,
@@ -360,7 +360,7 @@ func TestExtSvcAccountsService_SaveExternalService(t *testing.T) {
checks: func(t *testing.T, env *TestEnv) {
env.SaSvc.AssertCalled(t, "RetrieveServiceAccountIdByName", mock.Anything,
mock.MatchedBy(func(orgID int64) bool { return orgID == tmpOrgID }),
mock.MatchedBy(func(slug string) bool { return slug == extsvcPrefix+extSvcSlug }))
mock.MatchedBy(func(slug string) bool { return slug == ExtSvcPrefix+extSvcSlug }))
env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything,
mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool {
return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug &&