mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AuthN: Add service account token generation to ExtSvcAccountsService (#76327)
* Manage service account secrets * Wip * WIP * WIP * Revert to keep a light interface * Implement SaveExternalService * Remove unecessary functions from the interface * Remove unused field * Better log * Leave ext svc credentials out of the extsvcauth package for now * Remove todo * Add tests to SaveExternalService * Test that secret has been removed from store * Lint * Nit. * Rename commands and structs Co-authored-by: Kalle Persson <kalle.persson@grafana.com> * Account for PR feedback Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com> * Linting * Add nosec comment G101 - this is not a hardcoded secret * Lowercase kvStoreType --------- Co-authored-by: Kalle Persson <kalle.persson@grafana.com> Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
This commit is contained in:
@@ -4,4 +4,5 @@ import "github.com/grafana/grafana/pkg/util/errutil"
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUnknownProvider = errutil.BadRequest("extsvcauth.unknown-provider")
|
ErrUnknownProvider = errutil.BadRequest("extsvcauth.unknown-provider")
|
||||||
|
ErrCredentialsNotFound = errutil.NotFound("extsvcauth.credentials-not-found")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,24 @@ import (
|
|||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
)
|
)
|
||||||
|
|
||||||
type saveExtSvcAccountCmd struct {
|
const (
|
||||||
|
kvStoreType = "extsvc-token"
|
||||||
|
// #nosec G101 - this is not a hardcoded secret
|
||||||
|
tokenNamePrefix = "extsvc-token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credentials represents the credentials associated to an external service
|
||||||
|
type Credentials struct {
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SaveCredentialsCmd struct {
|
||||||
|
ExtSvcSlug string
|
||||||
|
OrgID int64
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
type saveCmd struct {
|
||||||
ExtSvcSlug string
|
ExtSvcSlug string
|
||||||
OrgID int64
|
OrgID int64
|
||||||
Permissions []ac.Permission
|
Permissions []ac.Permission
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/satokengen"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||||
"github.com/grafana/grafana/pkg/models/roletype"
|
"github.com/grafana/grafana/pkg/models/roletype"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||||
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
|
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,13 +20,16 @@ type ExtSvcAccountsService struct {
|
|||||||
acSvc ac.Service
|
acSvc ac.Service
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
saSvc sa.Service
|
saSvc sa.Service
|
||||||
|
skvStore kvstore.SecretsKVStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc sa.Service) *ExtSvcAccountsService {
|
func ProvideExtSvcAccountsService(acSvc ac.Service, saSvc sa.Service, db db.DB, secretsSvc secrets.Service) *ExtSvcAccountsService {
|
||||||
|
logger := log.New("serviceauth.extsvcaccounts")
|
||||||
return &ExtSvcAccountsService{
|
return &ExtSvcAccountsService{
|
||||||
acSvc: acSvc,
|
acSvc: acSvc,
|
||||||
logger: log.New("serviceauth.extsvcaccounts"),
|
logger: logger,
|
||||||
saSvc: saSvc,
|
saSvc: saSvc,
|
||||||
|
skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +49,46 @@ func (esa *ExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, org
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions.
|
||||||
|
func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) {
|
||||||
|
if cmd == nil {
|
||||||
|
esa.logger.Warn("Received no input")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slug := slugify.Slugify(cmd.Name)
|
||||||
|
|
||||||
|
if cmd.Impersonation.Enabled {
|
||||||
|
esa.logger.Warn("Impersonation setup skipped. It is not possible to impersonate with a service account token.", "service", slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
saID, err := esa.ManageExtSvcAccount(ctx, &extsvcauth.ManageExtSvcAccountCmd{
|
||||||
|
ExtSvcSlug: slug,
|
||||||
|
Enabled: cmd.Self.Enabled,
|
||||||
|
OrgID: extsvcauth.TmpOrgID,
|
||||||
|
Permissions: cmd.Self.Permissions,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need for a token if we don't have a service account
|
||||||
|
if saID <= 0 {
|
||||||
|
esa.logger.Debug("Skipping service account token creation", "service", slug)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := esa.getExtSvcAccountToken(ctx, extsvcauth.TmpOrgID, saID, slug)
|
||||||
|
if err != nil {
|
||||||
|
esa.logger.Error("Could not get the external svc token",
|
||||||
|
"service", slug,
|
||||||
|
"saID", saID,
|
||||||
|
"error", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &extsvcauth.ExternalService{Name: cmd.Name, ID: slug, Secret: token}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ManageExtSvcAccount creates, updates or deletes the service account associated with an external service
|
// ManageExtSvcAccount creates, updates or deletes the service account associated with an external service
|
||||||
func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *extsvcauth.ManageExtSvcAccountCmd) (int64, error) {
|
func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *extsvcauth.ManageExtSvcAccountCmd) (int64, error) {
|
||||||
if cmd == nil {
|
if cmd == nil {
|
||||||
@@ -71,7 +119,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
saID, errSave := esa.saveExtSvcAccount(ctx, &saveExtSvcAccountCmd{
|
saID, errSave := esa.saveExtSvcAccount(ctx, &saveCmd{
|
||||||
ExtSvcSlug: cmd.ExtSvcSlug,
|
ExtSvcSlug: cmd.ExtSvcSlug,
|
||||||
OrgID: cmd.OrgID,
|
OrgID: cmd.OrgID,
|
||||||
Permissions: cmd.Permissions,
|
Permissions: cmd.Permissions,
|
||||||
@@ -86,7 +134,7 @@ func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// saveExtSvcAccount creates or updates the service account associated with an external service
|
// saveExtSvcAccount creates or updates the service account associated with an external service
|
||||||
func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *saveExtSvcAccountCmd) (int64, error) {
|
func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *saveCmd) (int64, error) {
|
||||||
if cmd.SaID <= 0 {
|
if cmd.SaID <= 0 {
|
||||||
// Create a service account
|
// Create a service account
|
||||||
esa.logger.Debug("Create service account", "service", cmd.ExtSvcSlug, "orgID", cmd.OrgID)
|
esa.logger.Debug("Create service account", "service", cmd.ExtSvcSlug, "orgID", cmd.OrgID)
|
||||||
@@ -118,9 +166,75 @@ func (esa *ExtSvcAccountsService) saveExtSvcAccount(ctx context.Context, cmd *sa
|
|||||||
|
|
||||||
// deleteExtSvcAccount deletes a service account by ID and removes its associated role
|
// deleteExtSvcAccount deletes a service account by ID and removes its associated role
|
||||||
func (esa *ExtSvcAccountsService) deleteExtSvcAccount(ctx context.Context, orgID int64, slug string, saID int64) error {
|
func (esa *ExtSvcAccountsService) deleteExtSvcAccount(ctx context.Context, orgID int64, slug string, saID int64) error {
|
||||||
esa.logger.Info("Delete service account", "service", slug, "saID", saID)
|
esa.logger.Info("Delete service account", "service", slug, "orgID", orgID, "saID", saID)
|
||||||
if err := esa.saSvc.DeleteServiceAccount(ctx, orgID, saID); err != nil {
|
if err := esa.saSvc.DeleteServiceAccount(ctx, orgID, saID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return esa.acSvc.DeleteExternalServiceRole(ctx, slug)
|
if err := esa.acSvc.DeleteExternalServiceRole(ctx, slug); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return esa.DeleteExtSvcCredentials(ctx, orgID, slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getExtSvcAccountToken get or create the token of an External Service
|
||||||
|
func (esa *ExtSvcAccountsService) getExtSvcAccountToken(ctx context.Context, orgID, saID int64, extSvcSlug string) (string, error) {
|
||||||
|
// Get credentials from store
|
||||||
|
credentials, err := esa.GetExtSvcCredentials(ctx, orgID, extSvcSlug)
|
||||||
|
if err != nil && !errors.Is(err, extsvcauth.ErrCredentialsNotFound) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if credentials != nil {
|
||||||
|
return credentials.Secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate token
|
||||||
|
esa.logger.Info("Generate new service account token", "service", extSvcSlug, "orgID", orgID)
|
||||||
|
newKeyInfo, err := satokengen.New(extSvcSlug)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
esa.logger.Debug("Add service account token", "service", extSvcSlug, "orgID", orgID)
|
||||||
|
if _, err := esa.saSvc.AddServiceAccountToken(ctx, saID, &sa.AddServiceAccountTokenCommand{
|
||||||
|
Name: tokenNamePrefix + "-" + extSvcSlug,
|
||||||
|
OrgId: orgID,
|
||||||
|
Key: newKeyInfo.HashedKey,
|
||||||
|
}); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := esa.SaveExtSvcCredentials(ctx, &SaveCredentialsCmd{
|
||||||
|
ExtSvcSlug: extSvcSlug,
|
||||||
|
OrgID: orgID,
|
||||||
|
Secret: newKeyInfo.ClientSecret,
|
||||||
|
}); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newKeyInfo.ClientSecret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExtSvcCredentials get the credentials of an External Service from an encrypted storage
|
||||||
|
func (esa *ExtSvcAccountsService) GetExtSvcCredentials(ctx context.Context, orgID int64, extSvcSlug string) (*Credentials, error) {
|
||||||
|
esa.logger.Debug("Get service account token from skv", "service", extSvcSlug, "orgID", orgID)
|
||||||
|
token, ok, err := esa.skvStore.Get(ctx, orgID, extSvcSlug, kvStoreType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil, extsvcauth.ErrCredentialsNotFound.Errorf("No credential found for in store %v", extSvcSlug)
|
||||||
|
}
|
||||||
|
return &Credentials{Secret: token}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveExtSvcCredentials stores the credentials of an External Service in an encrypted storage
|
||||||
|
func (esa *ExtSvcAccountsService) SaveExtSvcCredentials(ctx context.Context, cmd *SaveCredentialsCmd) error {
|
||||||
|
esa.logger.Debug("Save service account token in skv", "service", cmd.ExtSvcSlug, "orgID", cmd.OrgID)
|
||||||
|
return esa.skvStore.Set(ctx, cmd.OrgID, cmd.ExtSvcSlug, kvStoreType, cmd.Secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteExtSvcCredentials removes the credentials of an External Service from an encrypted storage
|
||||||
|
func (esa *ExtSvcAccountsService) DeleteExtSvcCredentials(ctx context.Context, orgID int64, extSvcSlug string) error {
|
||||||
|
esa.logger.Debug("Delete service account token from skv", "service", extSvcSlug, "orgID", orgID)
|
||||||
|
return esa.skvStore.Del(ctx, orgID, extSvcSlug, kvStoreType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import (
|
|||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/apikey"
|
||||||
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -23,6 +25,7 @@ type TestEnv struct {
|
|||||||
S *ExtSvcAccountsService
|
S *ExtSvcAccountsService
|
||||||
AcStore *actest.MockStore
|
AcStore *actest.MockStore
|
||||||
SaSvc *tests.MockServiceAccountService
|
SaSvc *tests.MockServiceAccountService
|
||||||
|
SkvStore *kvstore.FakeSecretsKVStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupTestEnv(t *testing.T) *TestEnv {
|
func setupTestEnv(t *testing.T) *TestEnv {
|
||||||
@@ -34,11 +37,13 @@ func setupTestEnv(t *testing.T) *TestEnv {
|
|||||||
env := &TestEnv{
|
env := &TestEnv{
|
||||||
AcStore: &actest.MockStore{},
|
AcStore: &actest.MockStore{},
|
||||||
SaSvc: &tests.MockServiceAccountService{},
|
SaSvc: &tests.MockServiceAccountService{},
|
||||||
|
SkvStore: kvstore.NewFakeSecretsKVStore(),
|
||||||
}
|
}
|
||||||
env.S = &ExtSvcAccountsService{
|
env.S = &ExtSvcAccountsService{
|
||||||
acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt),
|
acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt),
|
||||||
logger: log.New("extsvcaccounts.test"),
|
logger: log.New("extsvcaccounts.test"),
|
||||||
saSvc: env.SaSvc,
|
saSvc: env.SaSvc,
|
||||||
|
skvStore: env.SkvStore,
|
||||||
}
|
}
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
@@ -209,3 +214,196 @@ func TestExtSvcAccountsService_ManageExtSvcAccount(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtSvcAccountsService_SaveExternalService(t *testing.T) {
|
||||||
|
extSvcSlug := "grafana-test-app"
|
||||||
|
tmpOrgID := int64(1)
|
||||||
|
extSvcAccID := int64(10)
|
||||||
|
extSvcPerms := []ac.Permission{{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}}
|
||||||
|
extSvcAccount := &sa.ServiceAccountDTO{
|
||||||
|
Id: extSvcAccID,
|
||||||
|
Name: extSvcSlug,
|
||||||
|
Login: extSvcSlug,
|
||||||
|
OrgId: tmpOrgID,
|
||||||
|
IsDisabled: false,
|
||||||
|
Role: string(roletype.RoleNone),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
init func(env *TestEnv)
|
||||||
|
cmd extsvcauth.ExternalServiceRegistration
|
||||||
|
checks func(t *testing.T, env *TestEnv)
|
||||||
|
want *extsvcauth.ExternalService
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should remove service account when disabled",
|
||||||
|
init: func(env *TestEnv) {
|
||||||
|
// A previous service account was attached to this slug
|
||||||
|
env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil)
|
||||||
|
env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil)
|
||||||
|
// A token was previously stored in the secret store
|
||||||
|
_ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken")
|
||||||
|
},
|
||||||
|
cmd: extsvcauth.ExternalServiceRegistration{
|
||||||
|
Name: extSvcSlug,
|
||||||
|
Self: extsvcauth.SelfCfg{
|
||||||
|
Enabled: false,
|
||||||
|
Permissions: extSvcPerms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 == 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 }))
|
||||||
|
env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything,
|
||||||
|
mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug }))
|
||||||
|
_, ok, _ := env.SkvStore.Get(context.Background(), tmpOrgID, extSvcSlug, kvStoreType)
|
||||||
|
require.False(t, ok, "secret should have been removed from store")
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should remove service account when no permission",
|
||||||
|
init: func(env *TestEnv) {
|
||||||
|
// A previous service account was attached to this slug
|
||||||
|
env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).Return(extSvcAccID, nil)
|
||||||
|
env.SaSvc.On("DeleteServiceAccount", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||||
|
env.AcStore.On("DeleteExternalServiceRole", mock.Anything, mock.Anything).Return(nil)
|
||||||
|
},
|
||||||
|
cmd: extsvcauth.ExternalServiceRegistration{
|
||||||
|
Name: extSvcSlug,
|
||||||
|
Self: extsvcauth.SelfCfg{
|
||||||
|
Enabled: true,
|
||||||
|
Permissions: []ac.Permission{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 == 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 }))
|
||||||
|
env.AcStore.AssertCalled(t, "DeleteExternalServiceRole", mock.Anything,
|
||||||
|
mock.MatchedBy(func(slug string) bool { return slug == extSvcSlug }))
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should create new service account",
|
||||||
|
init: func(env *TestEnv) {
|
||||||
|
// No previous service account was attached to this slug
|
||||||
|
env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(int64(0), sa.ErrServiceAccountNotFound.Errorf("mock"))
|
||||||
|
env.SaSvc.On("CreateServiceAccount", mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(extSvcAccount, nil)
|
||||||
|
// Api Key was added without problem
|
||||||
|
env.SaSvc.On("AddServiceAccountToken", mock.Anything, mock.Anything, mock.Anything).Return(&apikey.APIKey{}, nil)
|
||||||
|
env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil)
|
||||||
|
},
|
||||||
|
cmd: extsvcauth.ExternalServiceRegistration{
|
||||||
|
Name: extSvcSlug,
|
||||||
|
Self: extsvcauth.SelfCfg{
|
||||||
|
Enabled: true,
|
||||||
|
Permissions: extSvcPerms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 == 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 == extSvcSlug && *cmd.Role == roletype.RoleNone
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything,
|
||||||
|
mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool {
|
||||||
|
return cmd.ServiceAccountID == extSvcAccount.Id && cmd.ExternalServiceID == extSvcSlug &&
|
||||||
|
cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 &&
|
||||||
|
cmd.Permissions[0] == extSvcPerms[0]
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
want: &extsvcauth.ExternalService{
|
||||||
|
Name: extSvcSlug,
|
||||||
|
ID: extSvcSlug,
|
||||||
|
Secret: "not empty",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should update service account",
|
||||||
|
init: func(env *TestEnv) {
|
||||||
|
// A previous service account was attached to this slug
|
||||||
|
env.SaSvc.On("RetrieveServiceAccountIdByName", mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(int64(11), nil)
|
||||||
|
env.AcStore.On("SaveExternalServiceRole", mock.Anything, mock.Anything).Return(nil)
|
||||||
|
// This time we don't add a token but rely on the secret store
|
||||||
|
_ = env.SkvStore.Set(context.Background(), tmpOrgID, extSvcSlug, kvStoreType, "ExtSvcSecretToken")
|
||||||
|
},
|
||||||
|
cmd: extsvcauth.ExternalServiceRegistration{
|
||||||
|
Name: extSvcSlug,
|
||||||
|
Self: extsvcauth.SelfCfg{
|
||||||
|
Enabled: true,
|
||||||
|
Permissions: extSvcPerms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 == extSvcSlug }))
|
||||||
|
env.AcStore.AssertCalled(t, "SaveExternalServiceRole", mock.Anything,
|
||||||
|
mock.MatchedBy(func(cmd ac.SaveExternalServiceRoleCommand) bool {
|
||||||
|
return cmd.ServiceAccountID == int64(11) && cmd.ExternalServiceID == extSvcSlug &&
|
||||||
|
cmd.OrgID == int64(ac.GlobalOrgID) && len(cmd.Permissions) == 1 &&
|
||||||
|
cmd.Permissions[0] == extSvcPerms[0]
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
want: &extsvcauth.ExternalService{
|
||||||
|
Name: extSvcSlug,
|
||||||
|
ID: extSvcSlug,
|
||||||
|
Secret: "not empty",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
env := setupTestEnv(t)
|
||||||
|
if tt.init != nil {
|
||||||
|
tt.init(env)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := env.S.SaveExternalService(ctx, &tt.cmd)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if tt.checks != nil {
|
||||||
|
tt.checks(t, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only check that there is a secret, not it's actual value
|
||||||
|
if tt.want != nil && len(tt.want.Secret) > 0 {
|
||||||
|
require.NotEmpty(t, got.Secret)
|
||||||
|
tt.want.Secret = got.Secret
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
OAuth2Server AuthProvider = "OAuth2Server"
|
OAuth2Server AuthProvider = "OAuth2Server"
|
||||||
|
|
||||||
|
// TmpOrgID is the orgID we use while global service accounts are not supported.
|
||||||
|
TmpOrgID int64 = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthProvider string
|
type AuthProvider string
|
||||||
|
|||||||
Reference in New Issue
Block a user