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:
parent
2af5feb147
commit
d85df0a560
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -11,6 +11,7 @@ jest.mock('app/core/core', () => ({
|
||||
licensedAccessControlEnabled: () => false,
|
||||
hasPermission: () => true,
|
||||
hasPermissionInMetadata: () => true,
|
||||
hasAccessInMetadata: () => false,
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -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 = ({
|
||||
/>
|
||||
)}
|
||||
<div className={styles.tokensListHeader}>
|
||||
<h4>Tokens</h4>
|
||||
<h3>Tokens</h3>
|
||||
<Button onClick={() => setIsTokenModalOpen(true)} disabled={tokenActionsDisabled}>
|
||||
Add service account token
|
||||
</Button>
|
||||
@ -199,6 +205,7 @@ export const ServiceAccountPageUnconnected = ({
|
||||
tokenActionsDisabled={tokenActionsDisabled}
|
||||
/>
|
||||
)}
|
||||
{canReadPermissions && <ServiceAccountPermissions serviceAccount={serviceAccount} />}
|
||||
</div>
|
||||
<ConfirmModal
|
||||
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 = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<>
|
||||
<div className={cx(styles.table, 'admin-list-table')}>
|
||||
<table className="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Account</th>
|
||||
<th>ID</th>
|
||||
<th>Roles</th>
|
||||
<th>Tokens</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!isLoading &&
|
||||
serviceAccounts.length !== 0 &&
|
||||
serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
|
||||
{!isLoading && serviceAccounts.length !== 0 && (
|
||||
<>
|
||||
<div className={cx(styles.table, 'admin-list-table')}>
|
||||
<table className="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Account</th>
|
||||
<th>ID</th>
|
||||
<th>Roles</th>
|
||||
<th>Tokens</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
|
||||
<ServiceAccountListItem
|
||||
serviceAccount={serviceAccount}
|
||||
key={serviceAccount.id}
|
||||
@ -259,10 +258,11 @@ export const ServiceAccountsListPageUnconnected = ({
|
||||
onAddTokenClick={onTokenAdd}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{currentServiceAccount && (
|
||||
<>
|
||||
<ConfirmModal
|
||||
|
@ -37,7 +37,7 @@ export function ServiceAccountProfile({
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<h4>Information</h4>
|
||||
<h3>Information</h3>
|
||||
<table className="filter-table">
|
||||
<tbody>
|
||||
<ServiceAccountProfileRow
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||
@ -17,7 +17,7 @@ export const ServiceAccountTokensTable = ({ tokens, timeZone, tokenActionsDisabl
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<table className={cx(styles.section, 'filter-table')}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@ -124,4 +124,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
neverExpire: css`
|
||||
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> {
|
||||
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<void> {
|
||||
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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user