grafana/pkg/services/serviceaccounts/database/store.go
Eric Leijonmarck 9d6ab92e39
Service accounts: Remove Add API keys buttons and remove one state of migrating for API keys tab (#63411)
* add: hide apikeys tab on start

* make use of store method

* added hiding of apikeys tab for new org creation

* missing err check

* removed unused files

* implemennted fake to make tests run

* move check for globalHideApikeys from org to admin

* refactor to remove the fake

* removed unused method calls for interface

* Update pkg/services/serviceaccounts/manager/service.go

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>

* Update pkg/services/serviceaccounts/manager/service.go

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>

* remove the checkglobal method

* removed duplicate global set const

* add count of apikeys for performance

* remove apikeys adding in UI

* added back deleted file

* added comment on component

* changed wording and copy for hiding and migrating service accounts

* refactor: remove migrationstatus in front/backend

This removes the migrationstatus state from the UI in favor of only
looking at the number of API keys to determine what to show to the user.
This simplifies the logic and makes less calls to the backend with each
page load. This was called both on the API keys page and the Service
accounts page.

- removes the state of migrationstatus from the UI
- removes the backend call
- removes the backend endpoint for migrationstatus

* Update pkg/services/apikey/apikeyimpl/xorm_store.go

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

* changes the contet to also be primary

* change id of version for footer component

---------

Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
Co-authored-by: Karl Persson <kalle.persson@grafana.com>
2023-03-01 15:34:53 +00:00

531 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/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/migrator"
"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 org.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(dialect migrator.Dialect) []string {
deletes := []string{
"DELETE FROM api_key WHERE service_account_id = ?",
}
deletes = append(deletes, serviceAccountDeletions(dialect)...)
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(s.sqlStore.GetDialect()) {
_, 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) 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
}
func serviceAccountDeletions(dialect migrator.Dialect) []string {
deletes := []string{
"DELETE FROM star WHERE user_id = ?",
"DELETE FROM " + dialect.Quote("user") + " WHERE id = ?",
"DELETE FROM org_user WHERE user_id = ?",
"DELETE FROM dashboard_acl WHERE user_id = ?",
"DELETE FROM preferences WHERE user_id = ?",
"DELETE FROM team_member WHERE user_id = ?",
"DELETE FROM user_auth WHERE user_id = ?",
"DELETE FROM user_auth_token WHERE user_id = ?",
"DELETE FROM quota WHERE user_id = ?",
}
return deletes
}