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"
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
}
2022-03-14 17:24:07 +00:00
func ServiceAccountDeletions ( ) [ ] string {
deletes := [ ] string {
"DELETE FROM api_key WHERE service_account_id = ?" ,
}
deletes = append ( deletes , sqlstore . UserDeletions ( ) ... )
return deletes
2021-11-11 15:10:24 +00:00
}
2022-03-14 17:24:07 +00:00
func ( s * ServiceAccountsStoreImpl ) DeleteServiceAccount ( ctx context . Context , orgID , serviceAccountID int64 ) error {
return s . sqlStore . WithTransactionalDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
user := models . User { }
has , err := sess . Where ( ` org_id = ? and id = ? and is_service_account = ? ` ,
orgID , serviceAccountID , s . sqlStore . Dialect . BooleanStr ( true ) ) . Get ( & user )
2021-11-11 15:10:24 +00:00
if err != nil {
return err
}
2022-03-14 17:24:07 +00:00
if ! has {
return serviceaccounts . ErrServiceAccountNotFound
}
for _ , sql := range ServiceAccountDeletions ( ) {
_ , err := sess . Exec ( sql , user . Id )
if err != nil {
return err
}
}
return nil
} )
2021-11-11 15:10:24 +00:00
}
2021-12-16 14:28:16 +01:00
func ( s * ServiceAccountsStoreImpl ) UpgradeServiceAccounts ( ctx context . Context ) error {
2022-03-14 17:24:07 +00:00
basicKeys := s . sqlStore . GetAllOrgsAPIKeys ( ctx )
2021-12-16 14:28:16 +01:00
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 {
2022-03-14 17:24:07 +00:00
basicKeys := s . sqlStore . GetAllOrgsAPIKeys ( ctx )
2022-01-20 16:51:18 +01:00
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 {
2022-03-14 17:24:07 +00:00
prefix := "sa-autogen-"
cmd := models . CreateUserCommand {
Login : fmt . Sprintf ( "%v-%v-%v" , prefix , key . OrgId , key . Name ) ,
Name : prefix + key . Name ,
OrgId : key . OrgId ,
DefaultOrgRole : string ( key . Role ) ,
IsServiceAccount : true ,
2022-01-20 16:51:18 +01:00
}
2022-03-14 17:24:07 +00:00
newSA , errCreateSA := s . sqlStore . CreateUser ( ctx , cmd )
if errCreateSA != nil {
return fmt . Errorf ( "failed to create service account: %w" , errCreateSA )
2022-01-20 16:51:18 +01:00
}
2022-03-14 17:24:07 +00:00
if errUpdateKey := s . assignApiKeyToServiceAccount ( ctx , key . Id , newSA . Id ) ; errUpdateKey != nil {
return fmt . Errorf (
"failed to attach new service account to API key for keyId: %d and newServiceAccountId: %d with error: %w" ,
key . Id , newSA . Id , errUpdateKey ,
)
}
s . log . Debug ( "Updated basic api key" , "keyId" , key . Id , "newServiceAccountId" , newSA . Id )
2022-01-20 16:51:18 +01:00
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-03-14 17:24:07 +00:00
quotedUser := s . sqlStore . Dialect . Quote ( "user" )
2022-02-18 10:43:33 +00:00
sess = dbSession .
2022-03-14 17:24:07 +00:00
Join ( "inner" , quotedUser , quotedUser + ".id = api_key.service_account_id" ) .
Where ( quotedUser + ".org_id=? AND " + quotedUser + ".id=?" , orgID , serviceAccountID ) .
2022-04-14 15:09:55 +02:00
Asc ( "api_key.name" )
2022-01-19 09:55:38 +01:00
return sess . Find ( & result )
} )
return result , err
}
2022-02-07 13:51:54 +00: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-04-12 19:34:04 +02:00
func ( s * ServiceAccountsStoreImpl ) RetrieveServiceAccountIdByName ( ctx context . Context , orgID int64 , name string ) ( int64 , error ) {
serviceAccount := & struct {
Id int64
} { }
err := s . sqlStore . WithDbSession ( ctx , func ( dbSession * sqlstore . DBSession ) error {
sess := dbSession . Table ( "user" )
whereConditions := [ ] string {
fmt . Sprintf ( "%s.name = ?" ,
s . sqlStore . Dialect . Quote ( "user" ) ) ,
fmt . Sprintf ( "%s.org_id = ?" ,
s . sqlStore . Dialect . Quote ( "user" ) ) ,
fmt . Sprintf ( "%s.is_service_account = %s" ,
s . sqlStore . Dialect . Quote ( "user" ) ,
s . sqlStore . Dialect . 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
}
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
2022-03-15 10:48:10 +01:00
if _ , err := sess . Where ( "org_id = ? AND user_id = ?" , orgID , serviceAccountID ) . Update ( & orgUser ) ; err != nil {
2022-02-17 12:19:58 +00:00
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-14 17:24:07 +00:00
func ( s * ServiceAccountsStoreImpl ) SearchOrgServiceAccounts (
2022-03-18 15:50:34 +01:00
ctx context . Context , orgID int64 , query string , filter serviceaccounts . ServiceAccountFilter , page int , limit int ,
2022-03-14 17:24:07 +00:00
signedInUser * models . SignedInUser ,
) ( * serviceaccounts . SearchServiceAccountsResult , error ) {
searchResult := & serviceaccounts . SearchServiceAccountsResult {
TotalCount : 0 ,
ServiceAccounts : make ( [ ] * serviceaccounts . ServiceAccountDTO , 0 ) ,
Page : page ,
PerPage : limit ,
}
2022-03-04 12:04:07 +01:00
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 = ?" )
2022-03-14 17:24:07 +00:00
whereParams = append ( whereParams , orgID )
2022-03-04 12:04:07 +01:00
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
2022-05-05 16:31:14 +01:00
if ! accesscontrol . IsDisabled ( s . sqlStore . Cfg ) {
2022-03-21 17:58:18 +01:00
acFilter , err := accesscontrol . Filter ( signedInUser , "org_user.user_id" , "serviceaccounts:id:" , 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 ... )
}
2022-03-14 17:24:07 +00:00
if query != "" {
queryWithWildcards := "%" + query + "%"
2022-03-04 12:04:07 +01:00
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 )
}
2022-03-18 15:50:34 +01:00
switch 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 )
2022-06-01 10:35:16 +03:00
case serviceaccounts . FilterOnlyDisabled :
whereConditions = append (
whereConditions ,
"is_disabled = ?" )
whereParams = append ( whereParams , s . sqlStore . Dialect . BooleanStr ( true ) )
2022-03-18 15:50:34 +01:00
default :
s . log . Warn ( "invalid filter user for service account filtering" , "service account search filtering" , filter )
}
2022-03-04 12:04:07 +01:00
if len ( whereConditions ) > 0 {
sess . Where ( strings . Join ( whereConditions , " AND " ) , whereParams ... )
}
2022-03-14 17:24:07 +00:00
if limit > 0 {
offset := limit * ( page - 1 )
sess . Limit ( limit , offset )
2022-03-04 12:04:07 +01:00
}
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" )
2022-03-14 17:24:07 +00:00
if err := sess . Find ( & searchResult . ServiceAccounts ) ; err != nil {
2022-03-04 12:04:07 +01:00
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
}
2022-03-14 17:24:07 +00:00
searchResult . TotalCount = count
2022-03-04 12:04:07 +01:00
return nil
} )
if err != nil {
return nil , err
}
2022-03-14 17:24:07 +00:00
return searchResult , nil
2022-03-04 12:04:07 +01:00
}
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
}