mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Service Accounts: Managed permissions for service accounts (#51818)
* backend changes * frontend changes * linting * nit * import order * allow SA creator to access the SA page * fix merge * tests * fix frontend tests Co-authored-by: alexanderzobnin alexanderzobnin@gmail.com
This commit is contained in:
@@ -450,7 +450,10 @@ var teamsEditAccessEvaluator = ac.EvalAll(
|
|||||||
var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead)
|
var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead)
|
||||||
|
|
||||||
// serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access
|
// 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
|
// Metadata helpers
|
||||||
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
|
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ import (
|
|||||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
secretsMigrator "github.com/grafana/grafana/pkg/services/secrets/migrator"
|
secretsMigrator "github.com/grafana/grafana/pkg/services/secrets/migrator"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
||||||
serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
|
serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
|
||||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@@ -238,6 +239,10 @@ var wireBasicSet = wire.NewSet(
|
|||||||
pluginSettings.ProvideService,
|
pluginSettings.ProvideService,
|
||||||
wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)),
|
wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)),
|
||||||
alerting.ProvideService,
|
alerting.ProvideService,
|
||||||
|
database.ProvideServiceAccountsStore,
|
||||||
|
wire.Bind(new(serviceaccounts.Store), new(*database.ServiceAccountsStoreImpl)),
|
||||||
|
ossaccesscontrol.ProvideServiceAccountPermissions,
|
||||||
|
wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)),
|
||||||
serviceaccountsmanager.ProvideServiceAccountsService,
|
serviceaccountsmanager.ProvideServiceAccountsService,
|
||||||
wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsmanager.ServiceAccountsService)),
|
wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsmanager.ServiceAccountsService)),
|
||||||
expr.ProvideService,
|
expr.ProvideService,
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ type DatasourcePermissionsService interface {
|
|||||||
PermissionsService
|
PermissionsService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServiceAccountPermissionsService interface {
|
||||||
|
PermissionsService
|
||||||
|
}
|
||||||
|
|
||||||
type PermissionsService interface {
|
type PermissionsService interface {
|
||||||
// GetPermissions returns all permissions for given resourceID
|
// GetPermissions returns all permissions for given resourceID
|
||||||
GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]ResourcePermission, error)
|
GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]ResourcePermission, error)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"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/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"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 {
|
func (e DatasourcePermissionsService) MapActions(permission accesscontrol.ResourcePermission) string {
|
||||||
return ""
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ServiceAccountsAPI struct {
|
type ServiceAccountsAPI struct {
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
service serviceaccounts.Service
|
service serviceaccounts.Service
|
||||||
accesscontrol accesscontrol.AccessControl
|
accesscontrol accesscontrol.AccessControl
|
||||||
RouterRegister routing.RouteRegister
|
RouterRegister routing.RouteRegister
|
||||||
store serviceaccounts.Store
|
store serviceaccounts.Store
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
permissionService accesscontrol.ServiceAccountPermissionsService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceAccountsAPI(
|
func NewServiceAccountsAPI(
|
||||||
@@ -34,14 +35,16 @@ func NewServiceAccountsAPI(
|
|||||||
accesscontrol accesscontrol.AccessControl,
|
accesscontrol accesscontrol.AccessControl,
|
||||||
routerRegister routing.RouteRegister,
|
routerRegister routing.RouteRegister,
|
||||||
store serviceaccounts.Store,
|
store serviceaccounts.Store,
|
||||||
|
permissionService accesscontrol.ServiceAccountPermissionsService,
|
||||||
) *ServiceAccountsAPI {
|
) *ServiceAccountsAPI {
|
||||||
return &ServiceAccountsAPI{
|
return &ServiceAccountsAPI{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
service: service,
|
service: service,
|
||||||
accesscontrol: accesscontrol,
|
accesscontrol: accesscontrol,
|
||||||
RouterRegister: routerRegister,
|
RouterRegister: routerRegister,
|
||||||
store: store,
|
store: store,
|
||||||
log: log.New("serviceaccounts.api"),
|
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)
|
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)
|
return response.JSON(http.StatusCreated, serviceAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
@@ -15,8 +16,11 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"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"
|
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/contexthandler/ctxkey"
|
||||||
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
@@ -35,7 +39,7 @@ var (
|
|||||||
func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
|
func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
|
||||||
store := sqlstore.InitTestDB(t)
|
store := sqlstore.InitTestDB(t)
|
||||||
kvStore := kvstore.ProvideService(store)
|
kvStore := kvstore.ProvideService(store)
|
||||||
saStore := database.NewServiceAccountsStore(store, kvStore)
|
saStore := database.ProvideServiceAccountsStore(store, kvStore)
|
||||||
svcmock := tests.ServiceAccountMock{}
|
svcmock := tests.ServiceAccountMock{}
|
||||||
|
|
||||||
autoAssignOrg := store.Cfg.AutoAssignOrg
|
autoAssignOrg := store.Cfg.AutoAssignOrg
|
||||||
@@ -150,7 +154,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
serviceAccountRequestScenario(t, http.MethodPost, serviceAccountPath, testUser, func(httpmethod string, endpoint string, user *tests.TestUser) {
|
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)
|
marshalled, err := json.Marshal(tc.body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -166,9 +170,25 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
|
|||||||
require.Equal(t, tc.expectedCode, actualCode, actualBody)
|
require.Equal(t, tc.expectedCode, actualCode, actualBody)
|
||||||
|
|
||||||
if actualCode == http.StatusCreated {
|
if actualCode == http.StatusCreated {
|
||||||
assert.NotEmpty(t, actualBody["id"])
|
sa := serviceaccounts.ServiceAccountDTO{}
|
||||||
assert.Equal(t, tc.body["name"], actualBody["name"].(string))
|
err = json.Unmarshal(actual.Body.Bytes(), &sa)
|
||||||
assert.Equal(t, tc.wantID, actualBody["login"].(string))
|
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 {
|
} else if actualCode == http.StatusBadRequest {
|
||||||
assert.Contains(t, tc.wantError, actualBody["error"].(string))
|
assert.Contains(t, tc.wantError, actualBody["error"].(string))
|
||||||
}
|
}
|
||||||
@@ -182,7 +202,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
|
|||||||
func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
|
func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
|
||||||
store := sqlstore.InitTestDB(t)
|
store := sqlstore.InitTestDB(t)
|
||||||
kvStore := kvstore.ProvideService(store)
|
kvStore := kvstore.ProvideService(store)
|
||||||
saStore := database.NewServiceAccountsStore(store, kvStore)
|
saStore := database.ProvideServiceAccountsStore(store, kvStore)
|
||||||
svcmock := tests.ServiceAccountMock{}
|
svcmock := tests.ServiceAccountMock{}
|
||||||
|
|
||||||
var requestResponse = func(server *web.Mux, httpMethod, requestpath string) *httptest.ResponseRecorder {
|
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,
|
routerRegister routing.RouteRegister,
|
||||||
acmock *accesscontrolmock.Mock,
|
acmock *accesscontrolmock.Mock,
|
||||||
sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) (*web.Mux, *ServiceAccountsAPI) {
|
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.RegisterAPIEndpoints()
|
||||||
|
|
||||||
a.cfg.ApiKeyMaxSecondsToLive = -1 // disable api key expiration
|
a.cfg.ApiKeyMaxSecondsToLive = -1 // disable api key expiration
|
||||||
@@ -259,6 +283,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
|
|||||||
m := web.New()
|
m := web.New()
|
||||||
signedUser := &models.SignedInUser{
|
signedUser := &models.SignedInUser{
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
|
UserId: 1,
|
||||||
OrgRole: models.ROLE_VIEWER,
|
OrgRole: models.ROLE_VIEWER,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +303,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
|
|||||||
func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
|
func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
|
||||||
store := sqlstore.InitTestDB(t)
|
store := sqlstore.InitTestDB(t)
|
||||||
kvStore := kvstore.ProvideService(store)
|
kvStore := kvstore.ProvideService(store)
|
||||||
saStore := database.NewServiceAccountsStore(store, kvStore)
|
saStore := database.ProvideServiceAccountsStore(store, kvStore)
|
||||||
svcmock := tests.ServiceAccountMock{}
|
svcmock := tests.ServiceAccountMock{}
|
||||||
type testRetrieveSATestCase struct {
|
type testRetrieveSATestCase struct {
|
||||||
desc string
|
desc string
|
||||||
@@ -369,7 +394,7 @@ func newString(s string) *string {
|
|||||||
func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) {
|
func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) {
|
||||||
store := sqlstore.InitTestDB(t)
|
store := sqlstore.InitTestDB(t)
|
||||||
kvStore := kvstore.ProvideService(store)
|
kvStore := kvstore.ProvideService(store)
|
||||||
saStore := database.NewServiceAccountsStore(store, kvStore)
|
saStore := database.ProvideServiceAccountsStore(store, kvStore)
|
||||||
svcmock := tests.ServiceAccountMock{}
|
svcmock := tests.ServiceAccountMock{}
|
||||||
type testUpdateSATestCase struct {
|
type testUpdateSATestCase struct {
|
||||||
desc string
|
desc string
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func createTokenforSA(t *testing.T, store serviceaccounts.Store, keyName string,
|
|||||||
func TestServiceAccountsAPI_CreateToken(t *testing.T) {
|
func TestServiceAccountsAPI_CreateToken(t *testing.T) {
|
||||||
store := sqlstore.InitTestDB(t)
|
store := sqlstore.InitTestDB(t)
|
||||||
kvStore := kvstore.ProvideService(store)
|
kvStore := kvstore.ProvideService(store)
|
||||||
saStore := database.NewServiceAccountsStore(store, kvStore)
|
saStore := database.ProvideServiceAccountsStore(store, kvStore)
|
||||||
svcmock := tests.ServiceAccountMock{}
|
svcmock := tests.ServiceAccountMock{}
|
||||||
sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true})
|
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)
|
store := sqlstore.InitTestDB(t)
|
||||||
kvStore := kvstore.ProvideService(store)
|
kvStore := kvstore.ProvideService(store)
|
||||||
svcMock := &tests.ServiceAccountMock{}
|
svcMock := &tests.ServiceAccountMock{}
|
||||||
saStore := database.NewServiceAccountsStore(store, kvStore)
|
saStore := database.ProvideServiceAccountsStore(store, kvStore)
|
||||||
sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true})
|
sa := tests.SetupUserServiceAccount(t, store, tests.TestUser{Login: "sa", IsServiceAccount: true})
|
||||||
|
|
||||||
type testCreateSAToken struct {
|
type testCreateSAToken struct {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type ServiceAccountsStoreImpl struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceAccountsStore(store *sqlstore.SQLStore, kvStore kvstore.KVStore) *ServiceAccountsStoreImpl {
|
func ProvideServiceAccountsStore(store *sqlstore.SQLStore, kvStore kvstore.KVStore) *ServiceAccountsStoreImpl {
|
||||||
return &ServiceAccountsStoreImpl{
|
return &ServiceAccountsStoreImpl{
|
||||||
sqlStore: store,
|
sqlStore: store,
|
||||||
kvStore: kvStore,
|
kvStore: kvStore,
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func setupTestDatabase(t *testing.T) (*sqlstore.SQLStore, *ServiceAccountsStoreI
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
db := sqlstore.InitTestDB(t)
|
db := sqlstore.InitTestDB(t)
|
||||||
kvStore := kvstore.ProvideService(db)
|
kvStore := kvstore.ProvideService(db)
|
||||||
return db, NewServiceAccountsStore(db, kvStore)
|
return db, ProvideServiceAccountsStore(db, kvStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStore_RetrieveServiceAccount(t *testing.T) {
|
func TestStore_RetrieveServiceAccount(t *testing.T) {
|
||||||
|
|||||||
@@ -23,11 +23,26 @@ func RegisterRoles(ac accesscontrol.AccessControl) error {
|
|||||||
Grants: []string{string(models.ROLE_ADMIN)},
|
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{
|
saWriter := accesscontrol.RoleRegistration{
|
||||||
Role: accesscontrol.RoleDTO{
|
Role: accesscontrol.RoleDTO{
|
||||||
Name: "fixed:serviceaccounts:writer",
|
Name: "fixed:serviceaccounts:writer",
|
||||||
DisplayName: "Service accounts 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",
|
Group: "Service accounts",
|
||||||
Permissions: accesscontrol.ConcatPermissions(saReader.Role.Permissions, []accesscontrol.Permission{
|
Permissions: accesscontrol.ConcatPermissions(saReader.Role.Permissions, []accesscontrol.Permission{
|
||||||
{
|
{
|
||||||
@@ -41,12 +56,20 @@ func RegisterRoles(ac accesscontrol.AccessControl) error {
|
|||||||
Action: serviceaccounts.ActionDelete,
|
Action: serviceaccounts.ActionDelete,
|
||||||
Scope: serviceaccounts.ScopeAll,
|
Scope: serviceaccounts.ScopeAll,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Action: serviceaccounts.ActionPermissionsRead,
|
||||||
|
Scope: serviceaccounts.ScopeAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: serviceaccounts.ActionPermissionsWrite,
|
||||||
|
Scope: serviceaccounts.ScopeAll,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
Grants: []string{string(models.ROLE_ADMIN)},
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"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/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/api"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,15 +20,15 @@ type ServiceAccountsService struct {
|
|||||||
|
|
||||||
func ProvideServiceAccountsService(
|
func ProvideServiceAccountsService(
|
||||||
cfg *setting.Cfg,
|
cfg *setting.Cfg,
|
||||||
store *sqlstore.SQLStore,
|
|
||||||
kvStore kvstore.KVStore,
|
|
||||||
ac accesscontrol.AccessControl,
|
ac accesscontrol.AccessControl,
|
||||||
routeRegister routing.RouteRegister,
|
routeRegister routing.RouteRegister,
|
||||||
usageStats usagestats.Service,
|
usageStats usagestats.Service,
|
||||||
|
serviceAccountsStore serviceaccounts.Store,
|
||||||
|
permissionService accesscontrol.ServiceAccountPermissionsService,
|
||||||
) (*ServiceAccountsService, error) {
|
) (*ServiceAccountsService, error) {
|
||||||
database.InitMetrics()
|
database.InitMetrics()
|
||||||
s := &ServiceAccountsService{
|
s := &ServiceAccountsService{
|
||||||
store: database.NewServiceAccountsStore(store, kvStore),
|
store: serviceAccountsStore,
|
||||||
log: log.New("serviceaccounts"),
|
log: log.New("serviceaccounts"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ func ProvideServiceAccountsService(
|
|||||||
|
|
||||||
usageStats.RegisterMetricsFunc(s.store.GetUsageMetrics)
|
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()
|
serviceaccountsAPI.RegisterAPIEndpoints()
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ActionRead = "serviceaccounts:read"
|
ActionRead = "serviceaccounts:read"
|
||||||
ActionWrite = "serviceaccounts:write"
|
ActionWrite = "serviceaccounts:write"
|
||||||
ActionCreate = "serviceaccounts:create"
|
ActionCreate = "serviceaccounts:create"
|
||||||
ActionDelete = "serviceaccounts:delete"
|
ActionDelete = "serviceaccounts:delete"
|
||||||
|
ActionPermissionsRead = "serviceaccounts.permissions:read"
|
||||||
|
ActionPermissionsWrite = "serviceaccounts.permissions:write"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServiceAccount struct {
|
type ServiceAccount struct {
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import { OrgRolePicker } from '../admin/OrgRolePicker';
|
|||||||
|
|
||||||
export interface Props {}
|
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) =>
|
const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) =>
|
||||||
getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa);
|
getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa);
|
||||||
@@ -44,7 +48,10 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => {
|
|||||||
setRoleOptions(options);
|
setRoleOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) {
|
if (
|
||||||
|
contextSrv.accessControlBuiltInRoleAssignmentEnabled() &&
|
||||||
|
contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)
|
||||||
|
) {
|
||||||
const builtInRoles = await fetchBuiltinRoles(currentOrgId);
|
const builtInRoles = await fetchBuiltinRoles(currentOrgId);
|
||||||
setBuiltinRoles(builtInRoles);
|
setBuiltinRoles(builtInRoles);
|
||||||
}
|
}
|
||||||
@@ -75,7 +82,11 @@ export const ServiceAccountCreatePage = ({}: Props): JSX.Element => {
|
|||||||
tokens: response.tokens,
|
tokens: response.tokens,
|
||||||
};
|
};
|
||||||
await updateServiceAccount(response.id, data);
|
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);
|
await updateUserRoles(pendingRoles, newAccount.id, newAccount.orgId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ jest.mock('app/core/core', () => ({
|
|||||||
licensedAccessControlEnabled: () => false,
|
licensedAccessControlEnabled: () => false,
|
||||||
hasPermission: () => true,
|
hasPermission: () => true,
|
||||||
hasPermissionInMetadata: () => true,
|
hasPermissionInMetadata: () => true,
|
||||||
|
hasAccessInMetadata: () => false,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { contextSrv } from 'app/core/core';
|
|||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import { AccessControlAction, ApiKey, Role, ServiceAccountDTO, StoreState } from 'app/types';
|
import { AccessControlAction, ApiKey, Role, ServiceAccountDTO, StoreState } from 'app/types';
|
||||||
|
|
||||||
|
import { ServiceAccountPermissions } from './ServiceAccountPermissions';
|
||||||
import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal';
|
import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal';
|
||||||
import { ServiceAccountProfile } from './components/ServiceAccountProfile';
|
import { ServiceAccountProfile } from './components/ServiceAccountProfile';
|
||||||
import { ServiceAccountTokensTable } from './components/ServiceAccountTokensTable';
|
import { ServiceAccountTokensTable } from './components/ServiceAccountTokensTable';
|
||||||
@@ -79,6 +80,11 @@ export const ServiceAccountPageUnconnected = ({
|
|||||||
!contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) || serviceAccount.isDisabled;
|
!contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite) || serviceAccount.isDisabled;
|
||||||
|
|
||||||
const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
|
const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
|
||||||
|
const canReadPermissions = contextSrv.hasAccessInMetadata(
|
||||||
|
AccessControlAction.ServiceAccountsPermissionsRead,
|
||||||
|
serviceAccount!,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServiceAccount(serviceAccountId);
|
loadServiceAccount(serviceAccountId);
|
||||||
@@ -186,7 +192,7 @@ export const ServiceAccountPageUnconnected = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={styles.tokensListHeader}>
|
<div className={styles.tokensListHeader}>
|
||||||
<h4>Tokens</h4>
|
<h3>Tokens</h3>
|
||||||
<Button onClick={() => setIsTokenModalOpen(true)} disabled={tokenActionsDisabled}>
|
<Button onClick={() => setIsTokenModalOpen(true)} disabled={tokenActionsDisabled}>
|
||||||
Add service account token
|
Add service account token
|
||||||
</Button>
|
</Button>
|
||||||
@@ -199,6 +205,7 @@ export const ServiceAccountPageUnconnected = ({
|
|||||||
tokenActionsDisabled={tokenActionsDisabled}
|
tokenActionsDisabled={tokenActionsDisabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{canReadPermissions && <ServiceAccountPermissions serviceAccount={serviceAccount} />}
|
||||||
</div>
|
</div>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Permissions } from 'app/core/components/AccessControl';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
|
import { AccessControlAction, ServiceAccountDTO } from '../../types';
|
||||||
|
|
||||||
|
type ServiceAccountPermissionsProps = {
|
||||||
|
serviceAccount: ServiceAccountDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServiceAccountPermissions = (props: ServiceAccountPermissionsProps) => {
|
||||||
|
const canSetPermissions = contextSrv.hasPermissionInMetadata(
|
||||||
|
AccessControlAction.ServiceAccountsPermissionsWrite,
|
||||||
|
props.serviceAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Permissions
|
||||||
|
title="Permissions"
|
||||||
|
addPermissionTitle="Add permission"
|
||||||
|
buttonLabel="Add permission"
|
||||||
|
resource="serviceaccounts"
|
||||||
|
resourceId={props.serviceAccount.id}
|
||||||
|
canSetPermissions={canSetPermissions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -230,23 +230,22 @@ export const ServiceAccountsListPageUnconnected = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<>
|
{!isLoading && serviceAccounts.length !== 0 && (
|
||||||
<div className={cx(styles.table, 'admin-list-table')}>
|
<>
|
||||||
<table className="filter-table filter-table--hover">
|
<div className={cx(styles.table, 'admin-list-table')}>
|
||||||
<thead>
|
<table className="filter-table filter-table--hover">
|
||||||
<tr>
|
<thead>
|
||||||
<th></th>
|
<tr>
|
||||||
<th>Account</th>
|
<th></th>
|
||||||
<th>ID</th>
|
<th>Account</th>
|
||||||
<th>Roles</th>
|
<th>ID</th>
|
||||||
<th>Tokens</th>
|
<th>Roles</th>
|
||||||
<th style={{ width: '34px' }} />
|
<th>Tokens</th>
|
||||||
</tr>
|
<th style={{ width: '34px' }} />
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{!isLoading &&
|
<tbody>
|
||||||
serviceAccounts.length !== 0 &&
|
{serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
|
||||||
serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
|
|
||||||
<ServiceAccountListItem
|
<ServiceAccountListItem
|
||||||
serviceAccount={serviceAccount}
|
serviceAccount={serviceAccount}
|
||||||
key={serviceAccount.id}
|
key={serviceAccount.id}
|
||||||
@@ -259,10 +258,11 @@ export const ServiceAccountsListPageUnconnected = ({
|
|||||||
onAddTokenClick={onTokenAdd}
|
onAddTokenClick={onTokenAdd}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
{currentServiceAccount && (
|
{currentServiceAccount && (
|
||||||
<>
|
<>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ServiceAccountProfile({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h4>Information</h4>
|
<h3>Information</h3>
|
||||||
<table className="filter-table">
|
<table className="filter-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ServiceAccountProfileRow
|
<ServiceAccountProfileRow
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data';
|
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||||
@@ -17,7 +17,7 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl
|
|||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="filter-table">
|
<table className={cx(styles.section, 'filter-table')}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@@ -124,4 +124,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
neverExpire: css`
|
neverExpire: css`
|
||||||
color: ${theme.colors.text.secondary};
|
color: ${theme.colors.text.secondary};
|
||||||
`,
|
`,
|
||||||
|
section: css`
|
||||||
|
margin-bottom: ${theme.spacing(4)};
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ export function fetchACOptions(): ThunkResult<void> {
|
|||||||
|
|
||||||
export function getApiKeysMigrationStatus(): ThunkResult<void> {
|
export function getApiKeysMigrationStatus(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus');
|
if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) {
|
||||||
dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated));
|
const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus');
|
||||||
|
dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,20 +63,22 @@ export function fetchServiceAccounts(
|
|||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
try {
|
try {
|
||||||
if (withLoadingIndicator) {
|
if (contextSrv.hasPermission(AccessControlAction.ServiceAccountsRead)) {
|
||||||
dispatch(serviceAccountsFetchBegin());
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
serviceAccountsFetchEnd();
|
dispatch(serviceAccountsFetchEnd());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,11 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/org/serviceaccounts',
|
path: '/org/serviceaccounts',
|
||||||
roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ServiceAccountsRead]),
|
roles: () =>
|
||||||
|
contextSrv.evaluatePermission(
|
||||||
|
() => ['Admin'],
|
||||||
|
[AccessControlAction.ServiceAccountsRead, AccessControlAction.ServiceAccountsCreate]
|
||||||
|
),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "ServiceAccountsPage" */ 'app/features/serviceaccounts/ServiceAccountsListPage')
|
import(/* webpackChunkName: "ServiceAccountsPage" */ 'app/features/serviceaccounts/ServiceAccountsListPage')
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export enum AccessControlAction {
|
|||||||
ServiceAccountsCreate = 'serviceaccounts:create',
|
ServiceAccountsCreate = 'serviceaccounts:create',
|
||||||
ServiceAccountsWrite = 'serviceaccounts:write',
|
ServiceAccountsWrite = 'serviceaccounts:write',
|
||||||
ServiceAccountsDelete = 'serviceaccounts:delete',
|
ServiceAccountsDelete = 'serviceaccounts:delete',
|
||||||
|
ServiceAccountsPermissionsRead = 'serviceaccounts.permissions:read',
|
||||||
|
ServiceAccountsPermissionsWrite = 'serviceaccounts.permissions:write',
|
||||||
|
|
||||||
OrgsRead = 'orgs:read',
|
OrgsRead = 'orgs:read',
|
||||||
OrgsPreferencesRead = 'orgs.preferences:read',
|
OrgsPreferencesRead = 'orgs.preferences:read',
|
||||||
@@ -64,6 +66,8 @@ export enum AccessControlAction {
|
|||||||
ActionTeamsRolesAdd = 'teams.roles:add',
|
ActionTeamsRolesAdd = 'teams.roles:add',
|
||||||
ActionTeamsRolesRemove = 'teams.roles:remove',
|
ActionTeamsRolesRemove = 'teams.roles:remove',
|
||||||
ActionUserRolesList = 'users.roles:read',
|
ActionUserRolesList = 'users.roles:read',
|
||||||
|
ActionUserRolesAdd = 'users.roles:add',
|
||||||
|
ActionUserRolesRemove = 'users.roles:remove',
|
||||||
|
|
||||||
DashboardsRead = 'dashboards:read',
|
DashboardsRead = 'dashboards:read',
|
||||||
DashboardsWrite = 'dashboards:write',
|
DashboardsWrite = 'dashboards:write',
|
||||||
|
|||||||
Reference in New Issue
Block a user