diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 6f08cfd6382..dffe67e03a8 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -5,6 +5,7 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/setting" ) @@ -462,6 +463,12 @@ var teamsEditAccessEvaluator = ac.EvalAll( ), ) +// apiKeyAccessEvaluator is used to protect the "Configuration > API keys" page access +var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead) + +// serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access +var serviceAccountAccessEvaluator = ac.EvalPermission(serviceaccounts.ActionRead) + // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext, diff --git a/pkg/api/api.go b/pkg/api/api.go index 4836363dfaf..a7de6111fab 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/web" ) @@ -62,9 +63,9 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index) r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index) r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index) - r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index) - r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index) - r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) + r.Get("/org/serviceaccounts", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index) + r.Get("/org/serviceaccounts/:serviceAccountId", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index) + r.Get("/org/apikeys/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead)), hs.Index) r.Get("/dashboard/import/", reqSignedIn, hs.Index) r.Get("/configuration", reqGrafanaAdmin, hs.Index) r.Get("/admin", reqGrafanaAdmin, hs.Index) diff --git a/pkg/api/index.go b/pkg/api/index.go index 3167a3012e0..6f1bf97ee54 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -149,8 +149,11 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) } func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool { - return (c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR)) && - hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) + if !hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) { + return false + } + hasAccess := ac.HasAccess(hs.AccessControl, c) + return hasAccess(ac.ReqOrgAdmin, serviceAccountAccessEvaluator) } func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool { @@ -291,7 +294,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } - if c.OrgRole == models.ROLE_ADMIN { + if hasAccess(ac.ReqOrgAdmin, apiKeyAccessEvaluator) { configNodes = append(configNodes, &dtos.NavLink{ Text: "API keys", Id: "apikeys", diff --git a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx index e5d875a9be6..d0de3b2e9e9 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListItem.tsx @@ -28,7 +28,10 @@ const ServiceAccountListItem = memo( const editUrl = `org/serviceaccounts/${serviceAccount.id}`; const styles = useStyles2(getStyles); const canUpdateRole = contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount); - const rolePickerDisabled = !canUpdateRole; + const displayRolePicker = + contextSrv.hasPermission(AccessControlAction.ActionRolesList) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesList); + const enableRolePicker = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate) && canUpdateRole; return ( @@ -61,26 +64,30 @@ const ServiceAccountListItem = memo( {serviceAccount.login} - - {contextSrv.licensedAccessControlEnabled() ? ( - onRoleChange(newRole, serviceAccount)} - roleOptions={roleOptions} - builtInRoles={builtInRoles} - disabled={rolePickerDisabled} - /> - ) : ( + {contextSrv.licensedAccessControlEnabled() ? ( + displayRolePicker && ( + + onRoleChange(newRole, serviceAccount)} + roleOptions={roleOptions} + builtInRoles={builtInRoles} + disabled={!enableRolePicker} + /> + + ) + ) : ( + onRoleChange(newRole, serviceAccount)} /> - )} - + + )} { return async (dispatch) => { try { - const options = await fetchRoleOptions(); - dispatch(acOptionsLoaded(options)); - const builtInRoles = await fetchBuiltinRoles(); - dispatch(builtInRolesLoaded(builtInRoles)); + if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { + const options = await fetchRoleOptions(); + dispatch(acOptionsLoaded(options)); + } + if ( + contextSrv.licensedAccessControlEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList) + ) { + const builtInRoles = await fetchBuiltinRoles(); + dispatch(builtInRolesLoaded(builtInRoles)); + } } catch (error) { console.error(error); } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index bb8964345ca..b749467036d 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -195,14 +195,14 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/org/apikeys', - roles: () => ['Editor', 'Admin'], + roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ActionAPIKeysRead]), component: SafeDynamicImport( () => import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage') ), }, { path: '/org/serviceaccounts', - roles: () => ['Editor', 'Admin'], + roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ServiceAccountsRead]), component: SafeDynamicImport( () => import(/* webpackChunkName: "ServiceAccountsPage" */ 'app/features/serviceaccounts/ServiceAccountsListPage') diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index b2fd9b0ca2f..969985ba21a 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -23,6 +23,7 @@ export enum AccessControlAction { UsersQuotasList = 'users.quotas:list', UsersQuotasUpdate = 'users.quotas:update', + ServiceAccountsRead = 'serviceaccounts:read', ServiceAccountsCreate = 'serviceaccounts:create', ServiceAccountsWrite = 'serviceaccounts:write', ServiceAccountsDelete = 'serviceaccounts:delete', @@ -107,6 +108,8 @@ export enum AccessControlAction { // External alerting notifications actions. AlertingNotificationsExternalWrite = 'alert.notifications.external:write', AlertingNotificationsExternalRead = 'alert.notifications.external:read', + + ActionAPIKeysRead = 'apikeys:read', } export interface Role {