2021-11-11 15:10:24 +00:00
package database
2022-01-19 09:55:38 +01:00
//nolint:goimports
2021-11-11 15:10:24 +00:00
import (
"context"
2022-03-08 11:07:58 +00:00
"errors"
2021-12-16 14:28:16 +01:00
"fmt"
2022-03-01 08:21:55 +00:00
"strings"
2022-01-19 09:55:38 +01:00
"time"
2021-11-11 15:10:24 +00:00
2021-12-16 14:28:16 +01:00
"github.com/grafana/grafana/pkg/infra/log"
2021-11-11 15:10:24 +00:00
"github.com/grafana/grafana/pkg/models"
2022-03-04 12:04:07 +01:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
2021-11-11 15:10:24 +00:00
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/sqlstore"
2022-01-19 09:55:38 +01:00
"xorm.io/xorm"
2021-11-11 15:10:24 +00:00
)
type ServiceAccountsStoreImpl struct {
sqlStore * sqlstore . SQLStore
2021-12-16 14:28:16 +01:00
log log . Logger
2021-11-11 15:10:24 +00:00
}
func NewServiceAccountsStore ( store * sqlstore . SQLStore ) * ServiceAccountsStoreImpl {
return & ServiceAccountsStoreImpl {
sqlStore : store ,
}
}
2022-03-08 11:07:58 +00:00
func ( s * ServiceAccountsStoreImpl ) CreateServiceAccount ( ctx context . Context , orgID int64 , name string ) ( saDTO * serviceaccounts . ServiceAccountDTO , err error ) {
generatedLogin := "sa-" + strings . ToLower ( name )
generatedLogin = strings . ReplaceAll ( generatedLogin , " " , "-" )
2021-12-14 14:39:25 +01:00
cmd := models . CreateUserCommand {
2022-02-07 14:12:39 +01:00
Login : generatedLogin ,
2022-03-08 11:07:58 +00:00
OrgId : orgID ,
Name : name ,
2021-12-14 14:39:25 +01:00
IsServiceAccount : true ,
}
2022-03-08 11:07:58 +00:00
2021-12-14 14:39:25 +01:00
newuser , err := s . sqlStore . CreateUser ( ctx , cmd )
if err != nil {
2022-03-08 11:07:58 +00:00
if errors . Is ( err , models . ErrUserAlreadyExists ) {
return nil , & ErrSAInvalidName { }
}
return nil , fmt . Errorf ( "failed to create service account: %w" , err )
2021-12-14 14:39:25 +01:00
}
2022-03-08 11:07:58 +00:00
2022-02-08 14:31:34 +01:00
return & serviceaccounts . ServiceAccountDTO {
Id : newuser . Id ,
Name : newuser . Name ,
Login : newuser . Login ,
OrgId : newuser . OrgId ,
Tokens : 0 ,
} , nil
2021-12-14 14:39:25 +01:00
}
2021-11-11 15:10:24 +00:00
func ( s * ServiceAccountsStoreImpl ) DeleteServiceAccount ( ctx context . Context , orgID , serviceaccountID int64 ) error {
return s . sqlStore . WithTransactionalDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
return deleteServiceAccountInTransaction ( sess , orgID , serviceaccountID )
} )
}
func deleteServiceAccountInTransaction ( sess * sqlstore . DBSession , orgID , serviceAccountID int64 ) error {
user := models . User { }
has , err := sess . Where ( ` org_id = ? and id = ? and is_service_account = true ` , orgID , serviceAccountID ) . Get ( & user )
if err != nil {
return err
}
if ! has {
return serviceaccounts . ErrServiceAccountNotFound
}
for _ , sql := range sqlstore . ServiceAccountDeletions ( ) {
_ , err := sess . Exec ( sql , user . Id )
if err != nil {
return err
}
}
return nil
}
2021-12-16 14:28:16 +01:00
func ( s * ServiceAccountsStoreImpl ) UpgradeServiceAccounts ( ctx context . Context ) error {
basicKeys := s . sqlStore . GetNonServiceAccountAPIKeys ( ctx )
if len ( basicKeys ) > 0 {
s . log . Info ( "Launching background thread to upgrade API keys to service accounts" , "numberKeys" , len ( basicKeys ) )
go func ( ) {
for _ , key := range basicKeys {
2022-01-20 16:51:18 +01:00
err := s . CreateServiceAccountFromApikey ( ctx , key )
2021-12-16 14:28:16 +01:00
if err != nil {
2022-01-20 16:51:18 +01:00
s . log . Error ( "migating to service accounts failed with error" , err )
2021-12-16 14:28:16 +01:00
}
}
} ( )
}
return nil
}
2022-01-12 13:23:00 +01:00
2022-01-20 16:51:18 +01:00
func ( s * ServiceAccountsStoreImpl ) ConvertToServiceAccounts ( ctx context . Context , keys [ ] int64 ) error {
basicKeys := s . sqlStore . GetNonServiceAccountAPIKeys ( ctx )
if len ( basicKeys ) == 0 {
return nil
}
if len ( basicKeys ) != len ( keys ) {
return fmt . Errorf ( "one of the keys already has a serviceaccount" )
}
for _ , key := range basicKeys {
if ! contains ( keys , key . Id ) {
s . log . Error ( "convert service accounts stopped for keyId %d as it is not part of the query to convert or already has a service account" , key . Id )
continue
}
err := s . CreateServiceAccountFromApikey ( ctx , key )
if err != nil {
s . log . Error ( "converting to service accounts failed with error" , err )
}
}
return nil
}
func ( s * ServiceAccountsStoreImpl ) CreateServiceAccountFromApikey ( ctx context . Context , key * models . ApiKey ) error {
sa , err := s . sqlStore . CreateServiceAccountForApikey ( ctx , key . OrgId , key . Name , key . Role )
if err != nil {
return fmt . Errorf ( "failed to create service account for API key with error : %w" , err )
}
err = s . sqlStore . UpdateApikeyServiceAccount ( ctx , key . Id , sa . Id )
if err != nil {
return fmt . Errorf ( "failed to attach new service account to API key for keyId: %d and newServiceAccountId: %d with error: %w" , key . Id , sa . Id , err )
}
s . log . Debug ( "Updated basic api key" , "keyId" , key . Id , "newServiceAccountId" , sa . Id )
return nil
}
2022-01-19 09:55:38 +01:00
//nolint:gosimple
2022-02-04 12:06:30 +01:00
func ( s * ServiceAccountsStoreImpl ) ListTokens ( ctx context . Context , orgID int64 , serviceAccountID int64 ) ( [ ] * models . ApiKey , error ) {
2022-01-19 09:55:38 +01:00
result := make ( [ ] * models . ApiKey , 0 )
err := s . sqlStore . WithDbSession ( ctx , func ( dbSession * sqlstore . DBSession ) error {
var sess * xorm . Session
2022-02-18 10:43:33 +00:00
sess = dbSession .
2022-01-19 09:55:38 +01:00
Join ( "inner" , "user" , "user.id = api_key.service_account_id" ) .
2022-02-18 10:43:33 +00:00
Where ( "user.org_id=? AND user.id=?" , orgID , serviceAccountID ) .
2022-01-19 09:55:38 +01:00
Asc ( "name" )
return sess . Find ( & result )
} )
return result , err
}
2022-02-07 13:51:54 +00:00
2022-02-08 14:31:34 +01:00
func ( s * ServiceAccountsStoreImpl ) ListServiceAccounts ( ctx context . Context , orgID , serviceAccountID int64 ) ( [ ] * serviceaccounts . ServiceAccountDTO , error ) {
2022-01-12 13:23:00 +01:00
query := models . GetOrgUsersQuery { OrgId : orgID , IsServiceAccount : true }
2022-02-04 12:06:30 +01:00
if serviceAccountID > 0 {
query . UserID = serviceAccountID
}
2022-02-08 19:19:22 +00:00
if err := s . sqlStore . GetOrgUsers ( ctx , & query ) ; err != nil {
2022-01-12 13:23:00 +01:00
return nil , err
}
2022-02-08 19:19:22 +00:00
2022-02-08 14:31:34 +01:00
saDTOs := make ( [ ] * serviceaccounts . ServiceAccountDTO , len ( query . Result ) )
for i , user := range query . Result {
saDTOs [ i ] = & serviceaccounts . ServiceAccountDTO {
Id : user . UserId ,
OrgId : user . OrgId ,
Name : user . Name ,
Login : user . Login ,
2022-02-08 19:19:22 +00:00
Role : user . Role ,
2022-02-08 14:31:34 +01:00
}
tokens , err := s . ListTokens ( ctx , user . OrgId , user . UserId )
if err != nil {
return nil , err
}
saDTOs [ i ] . Tokens = int64 ( len ( tokens ) )
}
2022-02-08 19:19:22 +00:00
return saDTOs , nil
2022-01-12 13:23:00 +01:00
}
2022-01-19 09:23:46 +00:00
// RetrieveServiceAccountByID returns a service account by its ID
2022-02-08 14:31:34 +01:00
func ( s * ServiceAccountsStoreImpl ) RetrieveServiceAccount ( ctx context . Context , orgID , serviceAccountID int64 ) ( * serviceaccounts . ServiceAccountProfileDTO , error ) {
2022-03-01 08:21:55 +00:00
serviceAccount := & serviceaccounts . ServiceAccountProfileDTO { }
err := s . sqlStore . WithDbSession ( ctx , func ( dbSession * sqlstore . DBSession ) error {
sess := dbSession . Table ( "org_user" )
sess . Join ( "INNER" , s . sqlStore . Dialect . Quote ( "user" ) ,
fmt . Sprintf ( "org_user.user_id=%s.id" , s . sqlStore . Dialect . 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 . Dialect . Quote ( "user" ) ,
s . sqlStore . Dialect . 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
} )
2022-01-19 09:23:46 +00:00
if err != nil {
return nil , err
}
2022-02-22 13:58:42 +00:00
2022-02-25 10:33:34 +00:00
// Get Teams of service account. Can be optimized by combining with the query above
// in refactor
getTeamQuery := models . GetTeamsByUserQuery { UserId : serviceAccountID , OrgId : orgID }
if err := s . sqlStore . GetTeamsByUser ( ctx , & getTeamQuery ) ; err != nil {
return nil , err
}
teams := make ( [ ] string , len ( getTeamQuery . Result ) )
for i := range getTeamQuery . Result {
teams [ i ] = getTeamQuery . Result [ i ] . Name
}
2022-03-01 08:21:55 +00:00
serviceAccount . Teams = teams
return serviceAccount , nil
2022-01-19 09:23:46 +00:00
}
2022-01-20 16:51:18 +01:00
2022-02-17 12:19:58 +00:00
func ( s * ServiceAccountsStoreImpl ) UpdateServiceAccount ( ctx context . Context ,
orgID , serviceAccountID int64 ,
2022-03-01 08:21:55 +00:00
saForm * serviceaccounts . UpdateServiceAccountForm ) ( * serviceaccounts . ServiceAccountProfileDTO , error ) {
updatedUser := & serviceaccounts . ServiceAccountProfileDTO { }
2022-02-17 12:19:58 +00:00
err := s . sqlStore . WithTransactionalDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
2022-03-01 08:21:55 +00:00
var err error
updatedUser , err = s . RetrieveServiceAccount ( ctx , orgID , serviceAccountID )
if err != nil {
2022-02-17 12:19:58 +00:00
return err
}
2022-03-01 08:21:55 +00:00
if saForm . Name == nil && saForm . Role == nil && saForm . IsDisabled == nil {
2022-02-17 12:19:58 +00:00
return nil
}
updateTime := time . Now ( )
if saForm . Role != nil {
var orgUser models . OrgUser
orgUser . Role = * saForm . Role
orgUser . Updated = updateTime
if _ , err := sess . ID ( orgUser . Id ) . Update ( & orgUser ) ; err != nil {
return err
}
updatedUser . Role = string ( * saForm . Role )
}
2022-03-01 08:21:55 +00:00
if saForm . Name != nil || saForm . IsDisabled != nil {
2022-02-17 12:19:58 +00:00
user := models . User {
Updated : updateTime ,
}
2022-03-01 08:21:55 +00:00
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
}
2022-02-17 12:19:58 +00:00
if _ , err := sess . ID ( serviceAccountID ) . Update ( & user ) ; err != nil {
return err
}
}
return nil
} )
2022-03-01 08:21:55 +00:00
return updatedUser , err
2022-02-17 12:19:58 +00:00
}
2022-03-04 12:04:07 +01:00
func ( s * ServiceAccountsStoreImpl ) SearchOrgServiceAccounts ( ctx context . Context , query * models . SearchOrgUsersQuery ) ( [ ] * serviceaccounts . ServiceAccountDTO , error ) {
serviceAccounts := make ( [ ] * serviceaccounts . ServiceAccountDTO , 0 )
err := s . sqlStore . WithDbSession ( ctx , func ( dbSession * sqlstore . DBSession ) error {
sess := dbSession . Table ( "org_user" )
sess . Join ( "INNER" , s . sqlStore . Dialect . Quote ( "user" ) , fmt . Sprintf ( "org_user.user_id=%s.id" , s . sqlStore . Dialect . Quote ( "user" ) ) )
whereConditions := make ( [ ] string , 0 )
whereParams := make ( [ ] interface { } , 0 )
whereConditions = append ( whereConditions , "org_user.org_id = ?" )
whereParams = append ( whereParams , query . OrgID )
2022-03-08 13:10:16 +00:00
whereConditions = append ( whereConditions ,
fmt . Sprintf ( "%s.is_service_account = %s" ,
s . sqlStore . Dialect . Quote ( "user" ) ,
s . sqlStore . Dialect . BooleanStr ( true ) ) )
2022-03-04 12:04:07 +01:00
if s . sqlStore . Cfg . IsFeatureToggleEnabled ( featuremgmt . FlagAccesscontrol ) {
2022-03-14 17:11:21 +01:00
acFilter , err := accesscontrol . Filter ( query . User , "org_user.user_id" , "serviceaccounts" , serviceaccounts . ActionRead )
2022-03-04 12:04:07 +01:00
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 . Dialect . LikeStr ( ) + " ? OR name " + s . sqlStore . Dialect . LikeStr ( ) + " ? OR login " + s . sqlStore . Dialect . LikeStr ( ) + " ?)" )
whereParams = append ( whereParams , queryWithWildcards , queryWithWildcards , queryWithWildcards )
}
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" ,
2022-03-08 13:10:16 +00:00
"user.is_disabled" ,
2022-03-04 12:04:07 +01:00
)
sess . Asc ( "user.email" , "user.login" )
if err := sess . Find ( & serviceAccounts ) ; err != nil {
return err
}
// get total
serviceaccount := serviceaccounts . ServiceAccountDTO { }
countSess := dbSession . Table ( "org_user" )
sess . Join ( "INNER" , s . sqlStore . Dialect . Quote ( "user" ) , fmt . Sprintf ( "org_user.user_id=%s.id" , s . sqlStore . Dialect . Quote ( "user" ) ) )
if len ( whereConditions ) > 0 {
countSess . Where ( strings . Join ( whereConditions , " AND " ) , whereParams ... )
}
count , err := countSess . Count ( & serviceaccount )
if err != nil {
return err
}
query . Result . TotalCount = count
return nil
} )
if err != nil {
return nil , err
}
return serviceAccounts , nil
}
2022-01-20 16:51:18 +01:00
func contains ( s [ ] int64 , e int64 ) bool {
for _ , a := range s {
if a == e {
return true
}
}
return false
}