From 4a3961400a594102b5ec04ac796623f4c850bfad Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 14 Dec 2021 14:39:25 +0100 Subject: [PATCH] Serviceaccounts: create serviceaccount api (#42150) * WIP * wip * wip * wip * refactor: new return of the create service accoutn * refactor: change to have correct role * refactor: ability to create service accounts * make public * refactor: make ints instead * refactor: remove location sprintf * refactor: added back named constants --- pkg/services/serviceaccounts/api/api.go | 20 +++++++++++++++++++ .../serviceaccounts/database/database.go | 17 ++++++++++++++++ .../serviceaccounts/manager/service.go | 9 +++++++++ pkg/services/serviceaccounts/models.go | 11 ++++++++++ .../serviceaccounts/serviceaccounts.go | 8 +++++++- pkg/services/serviceaccounts/tests/common.go | 11 ++++++++++ 6 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index e5cbe825543..c06295cb07a 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -1,6 +1,7 @@ package api import ( + "errors" "net/http" "github.com/grafana/grafana/pkg/api/response" @@ -11,6 +12,7 @@ import ( acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web" ) type ServiceAccountsAPI struct { @@ -40,9 +42,27 @@ 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.Post("/", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate, serviceaccounts.ScopeID)), routing.Wrap(api.CreateServiceAccount)) }) } +// POST /api/serviceaccounts +func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) response.Response { + cmd := serviceaccounts.CreateServiceaccountForm{} + if err := web.Bind(c.Req, &cmd); err != nil { + return response.Error(http.StatusBadRequest, "Bad request data", err) + } + user, err := api.service.CreateServiceAccount(c.Req.Context(), &cmd) + switch { + case errors.Is(err, serviceaccounts.ErrServiceAccountNotFound): + return response.Error(http.StatusBadRequest, "Failed to create role with the provided name", err) + case err != nil: + return response.Error(http.StatusInternalServerError, "Failed to create service account", err) + } + + return response.JSON(http.StatusCreated, user) +} + func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) response.Response { scopeID := ctx.ParamsInt64(":serviceAccountId") err := api.service.DeleteServiceAccount(ctx.Req.Context(), ctx.OrgId, scopeID) diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index e04dbd79980..520478d0e8a 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -3,9 +3,11 @@ package database import ( "context" + "github.com/google/uuid" "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 { @@ -18,6 +20,21 @@ func NewServiceAccountsStore(store *sqlstore.SQLStore) *ServiceAccountsStoreImpl } } +func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, sa *serviceaccounts.CreateServiceaccountForm) (user *models.User, err error) { + // create a new service account - "user" with empty permissions + cmd := models.CreateUserCommand{ + Login: "Service-Account-" + uuid.New().String(), + Name: sa.Name + "-Service-Account-" + uuid.New().String(), + OrgId: sa.OrgID, + IsServiceAccount: true, + } + newuser, err := s.sqlStore.CreateUser(ctx, cmd) + if err != nil { + return nil, errors.Errorf("Failed to create user: %v", err) + } + return newuser, nil +} + 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) diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index c2ec6a0977b..67973ed30cf 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "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/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/api" @@ -42,6 +43,14 @@ func ProvideServiceAccountsService( return s, nil } +func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) { + if !sa.cfg.FeatureToggles["service-accounts"] { + sa.log.Debug(ServiceAccountFeatureToggleNotFound) + return nil, nil + } + return sa.store.CreateServiceAccount(ctx, saForm) +} + func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { if !sa.cfg.FeatureToggles["service-accounts"] { sa.log.Debug(ServiceAccountFeatureToggleNotFound) diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 6a5070a2e38..ded792a2182 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -13,3 +13,14 @@ const ( ActionCreate = "serviceaccounts:create" ActionDelete = "serviceaccounts:delete" ) + +type ServiceAccount struct { + Id int64 +} + +type CreateServiceaccountForm struct { + OrgID int64 `json:"-"` + Name string `json:"name" binding:"Required"` + DisplayName string `json:"displayName"` + Description string `json:"description"` +} diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index e173b0d3600..b0b52227dd8 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -1,10 +1,16 @@ package serviceaccounts -import "context" +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) type Service interface { + CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*models.User, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error } type Store interface { + CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*models.User, error) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error } diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 88dfdaeca4d..c843b324123 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -29,6 +29,10 @@ func SetupUserServiceAccount(t *testing.T, sqlStore *sqlstore.SQLStore, testUser // create mock for serviceaccountservice type ServiceAccountMock struct{} +func (s *ServiceAccountMock) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) { + return nil, nil +} + func (s *ServiceAccountMock) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { return nil } @@ -48,6 +52,7 @@ func SetupMockAccesscontrol(t *testing.T, userpermissionsfunc func(c context.Con var _ serviceaccounts.Store = new(ServiceAccountsStoreMock) type Calls struct { + CreateServiceAccount []interface{} DeleteServiceAccount []interface{} } @@ -55,6 +60,12 @@ type ServiceAccountsStoreMock struct { Calls Calls } +func (s *ServiceAccountsStoreMock) CreateServiceAccount(ctx context.Context, cmd *serviceaccounts.CreateServiceaccountForm) (*models.User, error) { + // now we can test that the mock has these calls when we call the function + s.Calls.CreateServiceAccount = append(s.Calls.CreateServiceAccount, []interface{}{ctx, cmd}) + return nil, nil +} + func (s *ServiceAccountsStoreMock) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { // now we can test that the mock has these calls when we call the function s.Calls.DeleteServiceAccount = append(s.Calls.DeleteServiceAccount, []interface{}{ctx, orgID, serviceAccountID})