diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index b750ab71410..8a8eb8f208d 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -450,7 +450,10 @@ var teamsEditAccessEvaluator = ac.EvalAll( var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead) // serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access -var serviceAccountAccessEvaluator = ac.EvalPermission(serviceaccounts.ActionRead) +var serviceAccountAccessEvaluator = ac.EvalAny( + ac.EvalPermission(serviceaccounts.ActionRead), + ac.EvalPermission(serviceaccounts.ActionCreate), +) // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 08ebbd15c0f..e7ce2a0a1b8 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -94,6 +94,7 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" secretsMigrator "github.com/grafana/grafana/pkg/services/secrets/migrator" "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/database" serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -238,6 +239,10 @@ var wireBasicSet = wire.NewSet( pluginSettings.ProvideService, wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), alerting.ProvideService, + database.ProvideServiceAccountsStore, + wire.Bind(new(serviceaccounts.Store), new(*database.ServiceAccountsStoreImpl)), + ossaccesscontrol.ProvideServiceAccountPermissions, + wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), serviceaccountsmanager.ProvideServiceAccountsService, wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsmanager.ServiceAccountsService)), expr.ProvideService, diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 5a49c3ee927..c4c30ed4c69 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -62,6 +62,10 @@ type DatasourcePermissionsService interface { PermissionsService } +type ServiceAccountPermissionsService interface { + PermissionsService +} + type PermissionsService interface { // GetPermissions returns all permissions for given resourceID GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]ResourcePermission, error) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index de8da4c50b4..5eed02aeecb 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -260,3 +261,46 @@ func (e DatasourcePermissionsService) SetPermissions(ctx context.Context, orgID func (e DatasourcePermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { return "" } + +type ServiceAccountPermissionsService struct { + *resourcepermissions.Service +} + +func ProvideServiceAccountPermissions( + cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, + ac accesscontrol.AccessControl, store resourcepermissions.Store, + license models.Licensing, serviceAccountStore serviceaccounts.Store, +) (*ServiceAccountPermissionsService, error) { + options := resourcepermissions.Options{ + Resource: "serviceaccounts", + ResourceAttribute: "id", + ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return err + } + _, err = serviceAccountStore.RetrieveServiceAccount(ctx, orgID, id) + return err + }, + Assignments: resourcepermissions.Assignments{ + Users: true, + Teams: false, + BuiltInRoles: false, + ServiceAccounts: false, + }, + PermissionsToActions: map[string][]string{ + "View": {serviceaccounts.ActionRead}, + "Edit": {serviceaccounts.ActionRead, serviceaccounts.ActionWrite, serviceaccounts.ActionDelete}, + "Admin": {serviceaccounts.ActionRead, serviceaccounts.ActionWrite, serviceaccounts.ActionDelete, serviceaccounts.ActionPermissionsRead, serviceaccounts.ActionPermissionsWrite}, + }, + ReaderRoleName: "Service account permission reader", + WriterRoleName: "Service account permission writer", + RoleGroup: "Service accounts", + } + + srv, err := resourcepermissions.New(options, cfg, router, license, ac, store, sql) + if err != nil { + return nil, err + } + return &ServiceAccountPermissionsService{srv}, nil +} diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index c926e4ce5ba..ba445fc1bfe 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -20,12 +20,13 @@ import ( ) type ServiceAccountsAPI struct { - cfg *setting.Cfg - service serviceaccounts.Service - accesscontrol accesscontrol.AccessControl - RouterRegister routing.RouteRegister - store serviceaccounts.Store - log log.Logger + cfg *setting.Cfg + service serviceaccounts.Service + accesscontrol accesscontrol.AccessControl + RouterRegister routing.RouteRegister + store serviceaccounts.Store + log log.Logger + permissionService accesscontrol.ServiceAccountPermissionsService } func NewServiceAccountsAPI( @@ -34,14 +35,16 @@ func NewServiceAccountsAPI( accesscontrol accesscontrol.AccessControl, routerRegister routing.RouteRegister, store serviceaccounts.Store, + permissionService accesscontrol.ServiceAccountPermissionsService, ) *ServiceAccountsAPI { return &ServiceAccountsAPI{ - cfg: cfg, - service: service, - accesscontrol: accesscontrol, - RouterRegister: routerRegister, - store: store, - log: log.New("serviceaccounts.api"), + cfg: cfg, + service: service, + accesscontrol: accesscontrol, + RouterRegister: routerRegister, + store: store, + log: log.New("serviceaccounts.api"), + permissionService: permissionService, } } @@ -103,6 +106,14 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon return response.Error(http.StatusInternalServerError, "Failed to create service account", err) } + if !api.accesscontrol.IsDisabled() { + if c.SignedInUser.IsRealUser() { + if _, err := api.permissionService.SetUserPermission(c.Req.Context(), c.OrgId, accesscontrol.User{ID: c.SignedInUser.UserId}, strconv.FormatInt(serviceAccount.Id, 10), "Admin"); err != nil { + return response.Error(http.StatusInternalServerError, "Failed to set permissions for service account creator", err) + } + } + } + return response.JSON(http.StatusCreated, serviceAccount) } diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index 2623440d866..7bbe322cc2b 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/grafana/grafana/pkg/api/routing" @@ -15,8 +16,11 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + acDatabase "github.com/grafana/grafana/pkg/services/accesscontrol/database" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" @@ -35,7 +39,7 @@ var ( func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} autoAssignOrg := store.Cfg.AutoAssignOrg @@ -150,7 +154,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { serviceAccountRequestScenario(t, http.MethodPost, serviceAccountPath, testUser, func(httpmethod string, endpoint string, user *tests.TestUser) { - server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) + server, api := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, saStore) marshalled, err := json.Marshal(tc.body) require.NoError(t, err) @@ -166,9 +170,25 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { require.Equal(t, tc.expectedCode, actualCode, actualBody) if actualCode == http.StatusCreated { - assert.NotEmpty(t, actualBody["id"]) - assert.Equal(t, tc.body["name"], actualBody["name"].(string)) - assert.Equal(t, tc.wantID, actualBody["login"].(string)) + sa := serviceaccounts.ServiceAccountDTO{} + err = json.Unmarshal(actual.Body.Bytes(), &sa) + require.NoError(t, err) + assert.NotZero(t, sa.Id) + assert.Equal(t, tc.body["name"], sa.Name) + assert.Equal(t, tc.wantID, sa.Login) + tempUser := &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{ + 1: { + serviceaccounts.ActionRead: []string{serviceaccounts.ScopeAll}, + }, + }, + } + perms, err := api.permissionService.GetPermissions(context.Background(), tempUser, strconv.FormatInt(sa.Id, 10)) + assert.NoError(t, err) + assert.Equal(t, 1, len(perms), "should have added managed permissions for SA creator") + assert.Equal(t, int64(1), perms[0].ID) + assert.Equal(t, int64(1), perms[0].UserId) } else if actualCode == http.StatusBadRequest { assert.Contains(t, tc.wantError, actualBody["error"].(string)) } @@ -182,7 +202,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) { func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} var requestResponse = func(server *web.Mux, httpMethod, requestpath string) *httptest.ResponseRecorder { @@ -251,7 +271,11 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) (*web.Mux, *ServiceAccountsAPI) { - a := NewServiceAccountsAPI(setting.NewCfg(), svc, acmock, routerRegister, saStore) + cfg := setting.NewCfg() + saPermissionService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, routing.NewRouteRegister(), sqlStore, acmock, acDatabase.ProvideService(sqlStore), &licensing.OSSLicensingService{}, saStore) + require.NoError(t, err) + + a := NewServiceAccountsAPI(cfg, svc, acmock, routerRegister, saStore, saPermissionService) a.RegisterAPIEndpoints() a.cfg.ApiKeyMaxSecondsToLive = -1 // disable api key expiration @@ -259,6 +283,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, m := web.New() signedUser := &models.SignedInUser{ OrgId: 1, + UserId: 1, OrgRole: models.ROLE_VIEWER, } @@ -278,7 +303,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} type testRetrieveSATestCase struct { desc string @@ -369,7 +394,7 @@ func newString(s string) *string { func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} type testUpdateSATestCase struct { desc string diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 93f46680378..2c9f5c3107a 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -52,7 +52,7 @@ func createTokenforSA(t *testing.T, store serviceaccounts.Store, keyName string, func TestServiceAccountsAPI_CreateToken(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) svcmock := tests.ServiceAccountMock{} sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true}) @@ -169,7 +169,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) { store := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(store) svcMock := &tests.ServiceAccountMock{} - saStore := database.NewServiceAccountsStore(store, kvStore) + saStore := database.ProvideServiceAccountsStore(store, kvStore) sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true}) type testCreateSAToken struct { diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index 5fc89b7ddf3..279401d6d7d 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -23,7 +23,7 @@ type ServiceAccountsStoreImpl struct { log log.Logger } -func NewServiceAccountsStore(store *sqlstore.SQLStore, kvStore kvstore.KVStore) *ServiceAccountsStoreImpl { +func ProvideServiceAccountsStore(store *sqlstore.SQLStore, kvStore kvstore.KVStore) *ServiceAccountsStoreImpl { return &ServiceAccountsStoreImpl{ sqlStore: store, kvStore: kvStore, diff --git a/pkg/services/serviceaccounts/database/database_test.go b/pkg/services/serviceaccounts/database/database_test.go index 1c0c0f3b899..10b896e3d48 100644 --- a/pkg/services/serviceaccounts/database/database_test.go +++ b/pkg/services/serviceaccounts/database/database_test.go @@ -105,7 +105,7 @@ func setupTestDatabase(t *testing.T) (*sqlstore.SQLStore, *ServiceAccountsStoreI t.Helper() db := sqlstore.InitTestDB(t) kvStore := kvstore.ProvideService(db) - return db, NewServiceAccountsStore(db, kvStore) + return db, ProvideServiceAccountsStore(db, kvStore) } func TestStore_RetrieveServiceAccount(t *testing.T) { diff --git a/pkg/services/serviceaccounts/manager/roles.go b/pkg/services/serviceaccounts/manager/roles.go index 6509c1e8236..6f317700c92 100644 --- a/pkg/services/serviceaccounts/manager/roles.go +++ b/pkg/services/serviceaccounts/manager/roles.go @@ -23,11 +23,26 @@ func RegisterRoles(ac accesscontrol.AccessControl) error { Grants: []string{string(models.ROLE_ADMIN)}, } + saCreator := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:serviceaccounts:creator", + DisplayName: "Service accounts creator", + Description: "Create service accounts.", + Group: "Service accounts", + Permissions: []accesscontrol.Permission{ + { + Action: serviceaccounts.ActionCreate, + }, + }, + }, + Grants: []string{string(models.ROLE_ADMIN)}, + } + saWriter := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Name: "fixed:serviceaccounts:writer", DisplayName: "Service accounts writer", - Description: "Create, delete, read, or query service accounts.", + Description: "Create, delete and read service accounts, manage service account permissions.", Group: "Service accounts", Permissions: accesscontrol.ConcatPermissions(saReader.Role.Permissions, []accesscontrol.Permission{ { @@ -41,12 +56,20 @@ func RegisterRoles(ac accesscontrol.AccessControl) error { Action: serviceaccounts.ActionDelete, Scope: serviceaccounts.ScopeAll, }, + { + Action: serviceaccounts.ActionPermissionsRead, + Scope: serviceaccounts.ScopeAll, + }, + { + Action: serviceaccounts.ActionPermissionsWrite, + Scope: serviceaccounts.ScopeAll, + }, }), }, Grants: []string{string(models.ROLE_ADMIN)}, } - if err := ac.DeclareFixedRoles(saReader, saWriter); err != nil { + if err := ac.DeclareFixedRoles(saReader, saCreator, saWriter); err != nil { return err } diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 7c578c30931..fd546c4fd00 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -4,14 +4,12 @@ import ( "context" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/api" "github.com/grafana/grafana/pkg/services/serviceaccounts/database" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -22,15 +20,15 @@ type ServiceAccountsService struct { func ProvideServiceAccountsService( cfg *setting.Cfg, - store *sqlstore.SQLStore, - kvStore kvstore.KVStore, ac accesscontrol.AccessControl, routeRegister routing.RouteRegister, usageStats usagestats.Service, + serviceAccountsStore serviceaccounts.Store, + permissionService accesscontrol.ServiceAccountPermissionsService, ) (*ServiceAccountsService, error) { database.InitMetrics() s := &ServiceAccountsService{ - store: database.NewServiceAccountsStore(store, kvStore), + store: serviceAccountsStore, log: log.New("serviceaccounts"), } @@ -40,7 +38,7 @@ func ProvideServiceAccountsService( usageStats.RegisterMetricsFunc(s.store.GetUsageMetrics) - serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store) + serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store, permissionService) serviceaccountsAPI.RegisterAPIEndpoints() return s, nil diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 39d78c4cf12..1eab5a23b68 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -13,10 +13,12 @@ var ( ) const ( - ActionRead = "serviceaccounts:read" - ActionWrite = "serviceaccounts:write" - ActionCreate = "serviceaccounts:create" - ActionDelete = "serviceaccounts:delete" + ActionRead = "serviceaccounts:read" + ActionWrite = "serviceaccounts:write" + ActionCreate = "serviceaccounts:create" + ActionDelete = "serviceaccounts:delete" + ActionPermissionsRead = "serviceaccounts.permissions:read" + ActionPermissionsWrite = "serviceaccounts.permissions:write" ) type ServiceAccount struct { diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index 5d2faca95f7..a2d3427990b 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -13,7 +13,11 @@ import { OrgRolePicker } from '../admin/OrgRolePicker'; export interface Props {} -const createServiceAccount = async (sa: ServiceAccountDTO) => getBackendSrv().post('/api/serviceaccounts/', sa); +const createServiceAccount = async (sa: ServiceAccountDTO) => { + const result = await getBackendSrv().post('/api/serviceaccounts/', sa); + await contextSrv.fetchUserPermissions(); + return result; +}; const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) => getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa); @@ -44,7 +48,10 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { setRoleOptions(options); } - if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { + if ( + contextSrv.accessControlBuiltInRoleAssignmentEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList) + ) { const builtInRoles = await fetchBuiltinRoles(currentOrgId); setBuiltinRoles(builtInRoles); } @@ -75,7 +82,11 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { tokens: response.tokens, }; await updateServiceAccount(response.id, data); - if (contextSrv.licensedAccessControlEnabled()) { + if ( + contextSrv.licensedAccessControlEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove) + ) { await updateUserRoles(pendingRoles, newAccount.id, newAccount.orgId); } } catch (e) { diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index d13d484cdc4..a654a5c7278 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -11,6 +11,7 @@ jest.mock('app/core/core', () => ({ licensedAccessControlEnabled: () => false, hasPermission: () => true, hasPermissionInMetadata: () => true, + hasAccessInMetadata: () => false, }, })); diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 75d4d9add87..aaeae3bc4ae 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -9,6 +9,7 @@ import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AccessControlAction, ApiKey, Role, ServiceAccountDTO, StoreState } from 'app/types'; +import { ServiceAccountPermissions } from './ServiceAccountPermissions'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; import { ServiceAccountProfile } from './components/ServiceAccountProfile'; import { ServiceAccountTokensTable } from './components/ServiceAccountTokensTable'; @@ -79,6 +80,11 @@ export const ServiceAccountPageUnconnected = ({ !contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) || serviceAccount.isDisabled; const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite); + const canReadPermissions = contextSrv.hasAccessInMetadata( + AccessControlAction.ServiceAccountsPermissionsRead, + serviceAccount!, + false + ); useEffect(() => { loadServiceAccount(serviceAccountId); @@ -186,7 +192,7 @@ export const ServiceAccountPageUnconnected = ({ /> )}
-

Tokens

+

Tokens

@@ -199,6 +205,7 @@ export const ServiceAccountPageUnconnected = ({ tokenActionsDisabled={tokenActionsDisabled} /> )} + {canReadPermissions && }
{ + const canSetPermissions = contextSrv.hasPermissionInMetadata( + AccessControlAction.ServiceAccountsPermissionsWrite, + props.serviceAccount + ); + + return ( + + ); +}; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 1d87856501a..04ad61af137 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -230,23 +230,22 @@ export const ServiceAccountsListPageUnconnected = ({ )} - <> -
- - - - - - - - - - - - {!isLoading && - serviceAccounts.length !== 0 && - serviceAccounts.map((serviceAccount: ServiceAccountDTO) => ( + {!isLoading && serviceAccounts.length !== 0 && ( + <> +
+
AccountIDRolesTokens -
+ + + + + + + + + + + {serviceAccounts.map((serviceAccount: ServiceAccountDTO) => ( ))} - -
AccountIDRolesTokens +
-
- + + + + + )} {currentServiceAccount && ( <> -

Information

+

Information

+
@@ -124,4 +124,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ neverExpire: css` color: ${theme.colors.text.secondary}; `, + section: css` + margin-bottom: ${theme.spacing(4)}; + `, }); diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 39129f7a8e7..3058878eddc 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -47,8 +47,10 @@ export function fetchACOptions(): ThunkResult { export function getApiKeysMigrationStatus(): ThunkResult { return async (dispatch) => { - const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus'); - dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated)); + if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) { + const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus'); + dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated)); + } }; } @@ -61,20 +63,22 @@ export function fetchServiceAccounts( ): ThunkResult { return async (dispatch, getState) => { try { - if (withLoadingIndicator) { - dispatch(serviceAccountsFetchBegin()); + if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) { + if (withLoadingIndicator) { + dispatch(serviceAccountsFetchBegin()); + } + const { perPage, page, query, serviceAccountStateFilter } = getState().serviceAccounts; + const result = await getBackendSrv().get( + `/api/serviceaccounts/search?perpage=${perPage}&page=${page}&query=${query}${getStateFilter( + serviceAccountStateFilter + )}&accesscontrol=true` + ); + dispatch(serviceAccountsFetched(result)); } - const { perPage, page, query, serviceAccountStateFilter } = getState().serviceAccounts; - const result = await getBackendSrv().get( - `/api/serviceaccounts/search?perpage=${perPage}&page=${page}&query=${query}${getStateFilter( - serviceAccountStateFilter - )}&accesscontrol=true` - ); - dispatch(serviceAccountsFetched(result)); } catch (error) { console.error(error); } finally { - serviceAccountsFetchEnd(); + dispatch(serviceAccountsFetchEnd()); } }; } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index b05e107eb9c..f3c63279213 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -210,7 +210,11 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/org/serviceaccounts', - roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ServiceAccountsRead]), + roles: () => + contextSrv.evaluatePermission( + () => ['Admin'], + [AccessControlAction.ServiceAccountsRead, AccessControlAction.ServiceAccountsCreate] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "ServiceAccountsPage" */ 'app/features/serviceaccounts/ServiceAccountsListPage') diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index c86c922880d..f7be002eb36 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -26,6 +26,8 @@ export enum AccessControlAction { ServiceAccountsCreate = 'serviceaccounts:create', ServiceAccountsWrite = 'serviceaccounts:write', ServiceAccountsDelete = 'serviceaccounts:delete', + ServiceAccountsPermissionsRead = 'serviceaccounts.permissions:read', + ServiceAccountsPermissionsWrite = 'serviceaccounts.permissions:write', OrgsRead = 'orgs:read', OrgsPreferencesRead = 'orgs.preferences:read', @@ -64,6 +66,8 @@ export enum AccessControlAction { ActionTeamsRolesAdd = 'teams.roles:add', ActionTeamsRolesRemove = 'teams.roles:remove', ActionUserRolesList = 'users.roles:read', + ActionUserRolesAdd = 'users.roles:add', + ActionUserRolesRemove = 'users.roles:remove', DashboardsRead = 'dashboards:read', DashboardsWrite = 'dashboards:write',
Name