mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
371d7850a5
* refactor: renaming of files from database to store * refactor: make service account store private - moves store interface to manager package - adds an interface to the ProvideAPI constructor - refactors tests to use the store when necessary - adds mocks for the new interface implementations in the tests package * wip * refactor: make fakestore in service * wip * wip * wip * working tests * trailing whitespaces * Update pkg/services/serviceaccounts/api/api.go * Update pkg/services/serviceaccounts/tests/common.go * Update pkg/services/serviceaccounts/tests/common.go * refactor: doc string for retriever * fix import unused * remove: serviceaccount from featuretoggle * added: back legacy serviceaccounts feature toggle * added: docs * refactor: make query for the SearchQuery * add: validation of service input fields * add validation
533 lines
16 KiB
Go
533 lines
16 KiB
Go
package database
|
|
|
|
//nolint:goimports
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/apikey"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
type ServiceAccountsStoreImpl struct {
|
|
cfg *setting.Cfg
|
|
sqlStore db.DB
|
|
apiKeyService apikey.Service
|
|
kvStore kvstore.KVStore
|
|
log log.Logger
|
|
orgService org.Service
|
|
userService user.Service
|
|
}
|
|
|
|
func ProvideServiceAccountsStore(cfg *setting.Cfg, store db.DB, apiKeyService apikey.Service,
|
|
kvStore kvstore.KVStore, userService user.Service, orgService org.Service) *ServiceAccountsStoreImpl {
|
|
return &ServiceAccountsStoreImpl{
|
|
cfg: cfg,
|
|
sqlStore: store,
|
|
apiKeyService: apiKeyService,
|
|
kvStore: kvStore,
|
|
log: log.New("serviceaccounts.store"),
|
|
orgService: orgService,
|
|
userService: userService,
|
|
}
|
|
}
|
|
|
|
// CreateServiceAccount creates service account
|
|
func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, orgId int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) {
|
|
generatedLogin := "sa-" + strings.ToLower(saForm.Name)
|
|
generatedLogin = strings.ReplaceAll(generatedLogin, " ", "-")
|
|
isDisabled := false
|
|
role := org.RoleViewer
|
|
if saForm.IsDisabled != nil {
|
|
isDisabled = *saForm.IsDisabled
|
|
}
|
|
if saForm.Role != nil {
|
|
role = *saForm.Role
|
|
}
|
|
var newSA *user.User
|
|
createErr := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) (err error) {
|
|
var errUser error
|
|
newSA, errUser = s.userService.CreateServiceAccount(ctx, &user.CreateUserCommand{
|
|
Login: generatedLogin,
|
|
OrgID: orgId,
|
|
Name: saForm.Name,
|
|
IsDisabled: isDisabled,
|
|
IsServiceAccount: true,
|
|
SkipOrgSetup: true,
|
|
})
|
|
if errUser != nil {
|
|
return errUser
|
|
}
|
|
|
|
errAddOrgUser := s.orgService.AddOrgUser(ctx, &org.AddOrgUserCommand{
|
|
Role: role,
|
|
OrgID: orgId,
|
|
UserID: newSA.ID,
|
|
AllowAddingServiceAccount: true,
|
|
})
|
|
if errAddOrgUser != nil {
|
|
return errAddOrgUser
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if createErr != nil {
|
|
if errors.Is(createErr, user.ErrUserAlreadyExists) {
|
|
return nil, ErrServiceAccountAlreadyExists
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to create service account: %w", createErr)
|
|
}
|
|
|
|
return &serviceaccounts.ServiceAccountDTO{
|
|
Id: newSA.ID,
|
|
Name: newSA.Name,
|
|
Login: newSA.Login,
|
|
OrgId: newSA.OrgID,
|
|
Tokens: 0,
|
|
Role: string(role),
|
|
IsDisabled: isDisabled,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateServiceAccount updates service account
|
|
func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(
|
|
ctx context.Context,
|
|
orgId, serviceAccountId int64,
|
|
saForm *serviceaccounts.UpdateServiceAccountForm,
|
|
) (*serviceaccounts.ServiceAccountProfileDTO, error) {
|
|
updatedUser := &serviceaccounts.ServiceAccountProfileDTO{}
|
|
|
|
err := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
|
var err error
|
|
updatedUser, err = s.RetrieveServiceAccount(ctx, orgId, serviceAccountId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if saForm.Name == nil && saForm.Role == nil && saForm.IsDisabled == nil {
|
|
return nil
|
|
}
|
|
|
|
updateTime := time.Now()
|
|
if saForm.Role != nil {
|
|
var orgUser models.OrgUser
|
|
orgUser.Role = *saForm.Role
|
|
orgUser.Updated = updateTime
|
|
|
|
if _, err := sess.Where("org_id = ? AND user_id = ?", orgId, serviceAccountId).Update(&orgUser); err != nil {
|
|
return err
|
|
}
|
|
|
|
updatedUser.Role = string(*saForm.Role)
|
|
}
|
|
|
|
if saForm.Name != nil || saForm.IsDisabled != nil {
|
|
user := user.User{
|
|
Updated: updateTime,
|
|
}
|
|
|
|
if saForm.IsDisabled != nil {
|
|
user.IsDisabled = *saForm.IsDisabled
|
|
updatedUser.IsDisabled = *saForm.IsDisabled
|
|
sess.UseBool("is_disabled")
|
|
}
|
|
|
|
if saForm.Name != nil {
|
|
user.Name = *saForm.Name
|
|
updatedUser.Name = *saForm.Name
|
|
}
|
|
|
|
if _, err := sess.ID(serviceAccountId).Update(&user); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return updatedUser, err
|
|
}
|
|
|
|
func ServiceAccountDeletions() []string {
|
|
deletes := []string{
|
|
"DELETE FROM api_key WHERE service_account_id = ?",
|
|
}
|
|
deletes = append(deletes, sqlstore.UserDeletions()...)
|
|
return deletes
|
|
}
|
|
|
|
// DeleteServiceAccount deletes service account and all associated tokens
|
|
func (s *ServiceAccountsStoreImpl) DeleteServiceAccount(ctx context.Context, orgId, serviceAccountId int64) error {
|
|
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
|
return s.deleteServiceAccount(sess, orgId, serviceAccountId)
|
|
})
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) deleteServiceAccount(sess *db.Session, orgId, serviceAccountId int64) error {
|
|
user := user.User{}
|
|
has, err := sess.Where(`org_id = ? and id = ? and is_service_account = ?`,
|
|
orgId, serviceAccountId, s.sqlStore.GetDialect().BooleanStr(true)).Get(&user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !has {
|
|
return serviceaccounts.ErrServiceAccountNotFound
|
|
}
|
|
for _, sql := range ServiceAccountDeletions() {
|
|
_, err := sess.Exec(sql, user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RetrieveServiceAccount returns a service account by its ID
|
|
func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgId, serviceAccountId int64) (*serviceaccounts.ServiceAccountProfileDTO, error) {
|
|
serviceAccount := &serviceaccounts.ServiceAccountProfileDTO{}
|
|
|
|
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
|
sess := dbSession.Table("org_user")
|
|
sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"),
|
|
fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user")))
|
|
|
|
whereConditions := make([]string, 0, 3)
|
|
whereParams := make([]interface{}, 0)
|
|
|
|
whereConditions = append(whereConditions, "org_user.org_id = ?")
|
|
whereParams = append(whereParams, orgId)
|
|
|
|
whereConditions = append(whereConditions, "org_user.user_id = ?")
|
|
whereParams = append(whereParams, serviceAccountId)
|
|
|
|
whereConditions = append(whereConditions,
|
|
fmt.Sprintf("%s.is_service_account = %s",
|
|
s.sqlStore.GetDialect().Quote("user"),
|
|
s.sqlStore.GetDialect().BooleanStr(true)))
|
|
|
|
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
|
|
|
sess.Cols(
|
|
"org_user.user_id",
|
|
"org_user.org_id",
|
|
"org_user.role",
|
|
"user.email",
|
|
"user.name",
|
|
"user.login",
|
|
"user.created",
|
|
"user.updated",
|
|
"user.is_disabled",
|
|
)
|
|
|
|
if ok, err := sess.Get(serviceAccount); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return serviceaccounts.ErrServiceAccountNotFound
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return serviceAccount, err
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) RetrieveServiceAccountIdByName(ctx context.Context, orgId int64, name string) (int64, error) {
|
|
serviceAccount := &struct {
|
|
Id int64
|
|
}{}
|
|
|
|
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
|
sess := dbSession.Table("user")
|
|
|
|
whereConditions := []string{
|
|
fmt.Sprintf("%s.name = ?",
|
|
s.sqlStore.GetDialect().Quote("user")),
|
|
fmt.Sprintf("%s.org_id = ?",
|
|
s.sqlStore.GetDialect().Quote("user")),
|
|
fmt.Sprintf("%s.is_service_account = %s",
|
|
s.sqlStore.GetDialect().Quote("user"),
|
|
s.sqlStore.GetDialect().BooleanStr(true)),
|
|
}
|
|
whereParams := []interface{}{name, orgId}
|
|
|
|
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
|
|
|
sess.Cols(
|
|
"user.id",
|
|
)
|
|
|
|
if ok, err := sess.Get(serviceAccount); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return serviceaccounts.ErrServiceAccountNotFound
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return serviceAccount.Id, nil
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) {
|
|
searchResult := &serviceaccounts.SearchOrgServiceAccountsResult{
|
|
TotalCount: 0,
|
|
ServiceAccounts: make([]*serviceaccounts.ServiceAccountDTO, 0),
|
|
Page: query.Page,
|
|
PerPage: query.Limit,
|
|
}
|
|
|
|
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
|
|
sess := dbSession.Table("org_user")
|
|
sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user")))
|
|
|
|
whereConditions := make([]string, 0)
|
|
whereParams := make([]interface{}, 0)
|
|
|
|
whereConditions = append(whereConditions, "org_user.org_id = ?")
|
|
whereParams = append(whereParams, query.OrgID)
|
|
|
|
whereConditions = append(whereConditions,
|
|
fmt.Sprintf("%s.is_service_account = %s",
|
|
s.sqlStore.GetDialect().Quote("user"),
|
|
s.sqlStore.GetDialect().BooleanStr(true)))
|
|
|
|
if !accesscontrol.IsDisabled(s.cfg) {
|
|
acFilter, err := accesscontrol.Filter(query.SignedInUser, "org_user.user_id", "serviceaccounts:id:", serviceaccounts.ActionRead)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
whereConditions = append(whereConditions, acFilter.Where)
|
|
whereParams = append(whereParams, acFilter.Args...)
|
|
}
|
|
|
|
if query.Query != "" {
|
|
queryWithWildcards := "%" + query.Query + "%"
|
|
whereConditions = append(whereConditions, "(email "+s.sqlStore.GetDialect().LikeStr()+" ? OR name "+s.sqlStore.GetDialect().LikeStr()+" ? OR login "+s.sqlStore.GetDialect().LikeStr()+" ?)")
|
|
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
|
}
|
|
|
|
switch query.Filter {
|
|
case serviceaccounts.FilterIncludeAll:
|
|
// pass
|
|
case serviceaccounts.FilterOnlyExpiredTokens:
|
|
now := time.Now().Unix()
|
|
// we do a subquery to remove duplicates coming from joining in api_keys, if we find more than one api key that has expired
|
|
whereConditions = append(
|
|
whereConditions,
|
|
"(SELECT count(*) FROM api_key WHERE api_key.service_account_id = org_user.user_id AND api_key.expires < ?) > 0")
|
|
whereParams = append(whereParams, now)
|
|
case serviceaccounts.FilterOnlyDisabled:
|
|
whereConditions = append(
|
|
whereConditions,
|
|
"is_disabled = ?")
|
|
whereParams = append(whereParams, s.sqlStore.GetDialect().BooleanStr(true))
|
|
default:
|
|
s.log.Warn("invalid filter user for service account filtering", "service account search filtering", query.Filter)
|
|
}
|
|
|
|
if len(whereConditions) > 0 {
|
|
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
|
}
|
|
if query.Limit > 0 {
|
|
offset := query.Limit * (query.Page - 1)
|
|
sess.Limit(query.Limit, offset)
|
|
}
|
|
|
|
sess.Cols(
|
|
"org_user.user_id",
|
|
"org_user.org_id",
|
|
"org_user.role",
|
|
"user.email",
|
|
"user.name",
|
|
"user.login",
|
|
"user.last_seen_at",
|
|
"user.is_disabled",
|
|
)
|
|
sess.Asc("user.email", "user.login")
|
|
if err := sess.Find(&searchResult.ServiceAccounts); err != nil {
|
|
return err
|
|
}
|
|
|
|
// get total
|
|
serviceaccount := serviceaccounts.ServiceAccountDTO{}
|
|
countSess := dbSession.Table("org_user")
|
|
sess.Join("INNER", s.sqlStore.GetDialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.GetDialect().Quote("user")))
|
|
|
|
if len(whereConditions) > 0 {
|
|
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
|
}
|
|
count, err := countSess.Count(&serviceaccount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
searchResult.TotalCount = count
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return searchResult, nil
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) GetAPIKeysMigrationStatus(ctx context.Context, orgId int64) (status *serviceaccounts.APIKeysMigrationStatus, err error) {
|
|
migrationStatus, exists, err := s.kvStore.Get(ctx, orgId, "serviceaccounts", "migrationStatus")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists && migrationStatus == "1" {
|
|
return &serviceaccounts.APIKeysMigrationStatus{
|
|
Migrated: true,
|
|
}, nil
|
|
} else {
|
|
return &serviceaccounts.APIKeysMigrationStatus{
|
|
Migrated: false,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) HideApiKeysTab(ctx context.Context, orgId int64) error {
|
|
if err := s.kvStore.Set(ctx, orgId, "serviceaccounts", "hideApiKeys", "1"); err != nil {
|
|
s.log.Error("Failed to hide API keys tab", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) MigrateApiKeysToServiceAccounts(ctx context.Context, orgId int64) error {
|
|
basicKeys, err := s.apiKeyService.GetAllAPIKeys(ctx, orgId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(basicKeys) > 0 {
|
|
for _, key := range basicKeys {
|
|
err := s.CreateServiceAccountFromApikey(ctx, key)
|
|
if err != nil {
|
|
s.log.Error("migating to service accounts failed with error", err)
|
|
return err
|
|
}
|
|
s.log.Debug("API key converted to service account token", "keyId", key.Id)
|
|
}
|
|
}
|
|
if err := s.kvStore.Set(ctx, orgId, "serviceaccounts", "migrationStatus", "1"); err != nil {
|
|
s.log.Error("Failed to write API keys migration status", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) MigrateApiKey(ctx context.Context, orgId int64, keyId int64) error {
|
|
basicKeys, err := s.apiKeyService.GetAllAPIKeys(ctx, orgId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(basicKeys) == 0 {
|
|
return fmt.Errorf("no API keys to convert found")
|
|
}
|
|
for _, key := range basicKeys {
|
|
if keyId == key.Id {
|
|
err := s.CreateServiceAccountFromApikey(ctx, key)
|
|
if err != nil {
|
|
s.log.Error("converting to service account failed with error", "keyId", keyId, "error", err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceAccountsStoreImpl) CreateServiceAccountFromApikey(ctx context.Context, key *apikey.APIKey) error {
|
|
prefix := "sa-autogen"
|
|
cmd := user.CreateUserCommand{
|
|
Login: fmt.Sprintf("%v-%v-%v", prefix, key.OrgId, key.Name),
|
|
Name: fmt.Sprintf("%v-%v", prefix, key.Name),
|
|
OrgID: key.OrgId,
|
|
DefaultOrgRole: string(key.Role),
|
|
IsServiceAccount: true,
|
|
}
|
|
|
|
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
|
newSA, errCreateSA := s.userService.CreateServiceAccount(ctx, &cmd)
|
|
if errCreateSA != nil {
|
|
return fmt.Errorf("failed to create service account: %w", errCreateSA)
|
|
}
|
|
|
|
if err := s.assignApiKeyToServiceAccount(sess, key.Id, newSA.ID); err != nil {
|
|
return fmt.Errorf("failed to migrate API key to service account token: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// RevertApiKey converts service account token to old API key
|
|
func (s *ServiceAccountsStoreImpl) RevertApiKey(ctx context.Context, saId int64, keyId int64) error {
|
|
query := apikey.GetByIDQuery{ApiKeyId: keyId}
|
|
if err := s.apiKeyService.GetApiKeyById(ctx, &query); err != nil {
|
|
return err
|
|
}
|
|
key := query.Result
|
|
|
|
if key.ServiceAccountId == nil {
|
|
return fmt.Errorf("API key is not service account token")
|
|
}
|
|
|
|
if *key.ServiceAccountId != saId {
|
|
return ErrServiceAccountAndTokenMismatch
|
|
}
|
|
|
|
tokens, err := s.ListTokens(ctx, &serviceaccounts.GetSATokensQuery{
|
|
OrgID: &key.OrgId,
|
|
ServiceAccountID: key.ServiceAccountId,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("cannot revert token: %w", err)
|
|
}
|
|
if len(tokens) > 1 {
|
|
return fmt.Errorf("cannot revert token: service account contains more than one token")
|
|
}
|
|
|
|
err = s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
|
user := user.User{}
|
|
has, err := sess.Where(`org_id = ? and id = ? and is_service_account = ?`,
|
|
key.OrgId, *key.ServiceAccountId, s.sqlStore.GetDialect().BooleanStr(true)).Get(&user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !has {
|
|
return serviceaccounts.ErrServiceAccountNotFound
|
|
}
|
|
// Detach API key from service account
|
|
if err := s.detachApiKeyFromServiceAccount(sess, key.Id); err != nil {
|
|
return err
|
|
}
|
|
// Delete service account
|
|
if err := s.deleteServiceAccount(sess, key.OrgId, *key.ServiceAccountId); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("cannot revert token to API key: %w", err)
|
|
}
|
|
return nil
|
|
}
|