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 = ({
>
)}
- <>
-
-
-
-
- |
- Account |
- ID |
- Roles |
- Tokens |
- |
-
-
-
- {!isLoading &&
- serviceAccounts.length !== 0 &&
- serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
+ {!isLoading && serviceAccounts.length !== 0 && (
+ <>
+
+
+
+
+ |
+ Account |
+ ID |
+ Roles |
+ Tokens |
+ |
+
+
+
+ {serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
))}
-
-
-
- >
+
+
+
+ >
+ )}
{currentServiceAccount && (
<>
- Information
+ Information
+
| Name |
@@ -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',