Skip to content

Commit

Permalink
AccessControl: upgrade apikeys by adding service accounts (grafana#42425
Browse files Browse the repository at this point in the history
)


Co-authored-by: Eric Leijonmarck <[email protected]>

Co-authored-by: Emil Tullstedt <[email protected]>

* Change default options for cloned service account

* Run in background

* Add endpoint to upgrade api keys to service accounts
  • Loading branch information
Jeremy Price authored Dec 16, 2021
1 parent 57def82 commit 13fdc52
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 14 deletions.
Empty file added log
Empty file.
5 changes: 4 additions & 1 deletion pkg/api/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
//Create a new service account for the new API key
serviceAccount, err := hs.SQLStore.CloneUserToServiceAccount(c.Req.Context(), c.SignedInUser)
if err != nil {
hs.log.Warn("Unable to clone user to service account", "err", err)
return response.Error(500, "Unable to clone user to service account", err)
}
cmd.ServiceAccountId = serviceAccount.Id
Expand All @@ -95,10 +96,12 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
query := models.GetUserByIdQuery{Id: cmd.ServiceAccountId}
err = bus.DispatchCtx(c.Req.Context(), &query)
if err != nil {
return response.Error(500, "Unable to clone user to service account", err)
hs.log.Warn("Unable to link new API key to existing service account", "err", err, "query", query)
return response.Error(500, "Unable to link new API key to existing service account", err)
}
serviceAccountDetails := query.Result
if serviceAccountDetails.OrgId != c.OrgId || serviceAccountDetails.OrgId != cmd.OrgId {
hs.log.Warn("Target service is not in the same organisation as requesting user or api key", "err", err, "reqOrg", cmd.OrgId, "serviceAccId", serviceAccountDetails.OrgId, "userOrgId", c.OrgId)
return response.Error(403, "Target service is not in the same organisation as requesting user or api key", err)
}
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/services/serviceaccounts/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ type ServiceAccountsAPI struct {
service serviceaccounts.Service
accesscontrol accesscontrol.AccessControl
RouterRegister routing.RouteRegister
store serviceaccounts.Store
}

func NewServiceAccountsAPI(
service serviceaccounts.Service,
accesscontrol accesscontrol.AccessControl,
routerRegister routing.RouteRegister,
store serviceaccounts.Store,
) *ServiceAccountsAPI {
return &ServiceAccountsAPI{
service: service,
accesscontrol: accesscontrol,
RouterRegister: routerRegister,
store: store,
}
}

Expand All @@ -42,6 +45,7 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
auth := acmiddleware.Middleware(api.accesscontrol)
api.RouterRegister.Group("/api/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) {
serviceAccountsRoute.Delete("/:serviceAccountId", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionDelete, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteServiceAccount))
serviceAccountsRoute.Get("/upgrade", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate, serviceaccounts.ScopeID)), routing.Wrap(api.UpgradeServiceAccounts))
serviceAccountsRoute.Post("/", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate, serviceaccounts.ScopeID)), routing.Wrap(api.CreateServiceAccount))
})
}
Expand Down Expand Up @@ -71,3 +75,11 @@ func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) resp
}
return response.Success("service account deleted")
}

func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) response.Response {
if err := api.store.UpgradeServiceAccounts(ctx.Req.Context()); err == nil {
return response.Success("service accounts upgraded")
} else {
return response.Error(500, "Internal server error", err)
}
}
9 changes: 4 additions & 5 deletions pkg/services/serviceaccounts/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/stretchr/testify/require"

"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
)

var (
Expand Down Expand Up @@ -93,11 +95,8 @@ func serviceAccountDeletionScenario(t *testing.T, httpMethod string, endpoint st
}

func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock) *web.Mux {
a := NewServiceAccountsAPI(
svc,
acmock,
routerRegister,
)
store := sqlstore.InitTestDB(t)
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(store))
a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}})

m := web.New()
Expand Down
30 changes: 28 additions & 2 deletions pkg/services/serviceaccounts/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ package database

import (
"context"
"fmt"

"github.com/google/uuid"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/pkg/errors"
)

type ServiceAccountsStoreImpl struct {
sqlStore *sqlstore.SQLStore
log log.Logger
}

func NewServiceAccountsStore(store *sqlstore.SQLStore) *ServiceAccountsStoreImpl {
Expand All @@ -30,7 +32,7 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, sa
}
newuser, err := s.sqlStore.CreateUser(ctx, cmd)
if err != nil {
return nil, errors.Errorf("Failed to create user: %v", err)
return nil, fmt.Errorf("failed to create user: %v", err)
}
return newuser, nil
}
Expand Down Expand Up @@ -58,3 +60,27 @@ func deleteServiceAccountInTransaction(sess *sqlstore.DBSession, orgID, serviceA
}
return nil
}

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 {
sa, err := s.sqlStore.CreateServiceAccountForApikey(ctx, key.OrgId, key.Name, key.Role)
if err != nil {
s.log.Error("Failed to create service account for API key", "err", err, "keyId", key.Id)
continue
}

err = s.sqlStore.UpdateApikeyServiceAccount(ctx, key.Id, sa.Id)
if err != nil {
s.log.Error("Failed to attach new service account to API key", "err", err, "keyId", key.Id, "newServiceAccountId", sa.Id)
continue
}
s.log.Debug("Updated basic api key", "keyId", key.Id, "newServiceAccountId", sa.Id)
}
}()
}
return nil
}
3 changes: 2 additions & 1 deletion pkg/services/serviceaccounts/manager/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ func ProvideServiceAccountsService(
if err := ac.DeclareFixedRoles(role); err != nil {
return nil, err
}
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister)
serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister, s.store)
serviceaccountsAPI.RegisterAPIEndpoints(cfg)

return s, nil
}

Expand Down
1 change: 1 addition & 0 deletions pkg/services/serviceaccounts/serviceaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type Service interface {
type Store interface {
CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*models.User, error)
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error
UpgradeServiceAccounts(ctx context.Context) error
}
10 changes: 8 additions & 2 deletions pkg/services/serviceaccounts/tests/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ func SetupMockAccesscontrol(t *testing.T, userpermissionsfunc func(c context.Con
var _ serviceaccounts.Store = new(ServiceAccountsStoreMock)

type Calls struct {
CreateServiceAccount []interface{}
DeleteServiceAccount []interface{}
CreateServiceAccount []interface{}
DeleteServiceAccount []interface{}
UpgradeServiceAccounts []interface{}
}

type ServiceAccountsStoreMock struct {
Expand All @@ -71,3 +72,8 @@ func (s *ServiceAccountsStoreMock) DeleteServiceAccount(ctx context.Context, org
s.Calls.DeleteServiceAccount = append(s.Calls.DeleteServiceAccount, []interface{}{ctx, orgID, serviceAccountID})
return nil
}

func (s *ServiceAccountsStoreMock) UpgradeServiceAccounts(ctx context.Context) error {
s.Calls.DeleteServiceAccount = append(s.Calls.UpgradeServiceAccounts, []interface{}{ctx})
return nil
}
42 changes: 41 additions & 1 deletion pkg/services/sqlstore/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"context"
"time"

"xorm.io/xorm"

"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"xorm.io/xorm"
)

func (ss *SQLStore) addAPIKeysQueryAndCommandHandlers() {
Expand Down Expand Up @@ -38,6 +39,21 @@ func (ss *SQLStore) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuer
})
}

// GetAPIKeys queries the database based
// on input on GetApiKeysQuery
func (ss *SQLStore) GetNonServiceAccountAPIKeys(ctx context.Context) []*models.ApiKey {
result := make([]*models.ApiKey, 0)
err := ss.WithDbSession(ctx, func(dbSession *DBSession) error {
sess := dbSession. //CHECK how many API keys do our clients have? Can we load them all?
Where("(expires IS NULL OR expires >= ?) AND service_account_id < 1 ", timeNow().Unix()).Asc("name")
return sess.Find(&result)
})
if err != nil {
ss.log.Warn("API key not loaded", "err", err)
}
return result
}

func (ss *SQLStore) DeleteApiKey(ctx context.Context, cmd *models.DeleteApiKeyCommand) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
return deleteAPIKey(sess, cmd.Id, cmd.OrgId)
Expand Down Expand Up @@ -96,6 +112,30 @@ func (ss *SQLStore) AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand)
})
}

// UpdateApikeyServiceAccount sets a service account for an existing API key
func (ss *SQLStore) UpdateApikeyServiceAccount(ctx context.Context, apikeyId int64, saccountId int64) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
key := models.ApiKey{Id: apikeyId}
exists, err := sess.Get(&key)
if err != nil {
ss.log.Warn("API key not loaded", "err", err)
return err
}
if !exists {
ss.log.Warn("API key not found", "err", err)
return models.ErrApiKeyNotFound
}
key.ServiceAccountId = saccountId

if _, err := sess.ID(key.Id).Update(&key); err != nil {
ss.log.Warn("Could not update api key", "err", err)
return err
}

return nil
})
}

func (ss *SQLStore) GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
var apikey models.ApiKey
Expand Down
23 changes: 21 additions & 2 deletions pkg/services/sqlstore/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
"time"

"github.com/google/uuid"

"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/pkg/errors"
)

func (ss *SQLStore) addUserQueryAndCommandHandlers() {
Expand Down Expand Up @@ -197,7 +197,26 @@ func (ss *SQLStore) CloneUserToServiceAccount(ctx context.Context, siUser *model

newuser, err := ss.CreateUser(ctx, cmd)
if err != nil {
return nil, errors.Errorf("Failed to create user: %v", err)
ss.log.Warn("user not cloned", "err", err)
return nil, fmt.Errorf("failed to create user: %w", err)
}

return newuser, err
}

func (ss *SQLStore) CreateServiceAccountForApikey(ctx context.Context, orgId int64, keyname string, role models.RoleType) (*models.User, error) {
prefix := "Service-Account-Autogen-"
cmd := models.CreateUserCommand{
Login: fmt.Sprintf("%v-%v-%v", prefix, orgId, keyname),
Name: prefix + keyname,
OrgId: orgId,
DefaultOrgRole: string(role),
IsServiceAccount: true,
}

newuser, err := ss.CreateUser(ctx, cmd)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}

return newuser, err
Expand Down

0 comments on commit 13fdc52

Please sign in to comment.