mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Access control: expose SA frontend to users with the right permissions (#47727)
* expose frontend to users with permissions * cover the ui endpoints * fix permissions
This commit is contained in:
parent
6f31a69bfd
commit
e50bd5cac8
@ -5,6 +5,7 @@ import (
|
|||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"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
|
// Metadata helpers
|
||||||
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
|
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
|
||||||
func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext,
|
func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext,
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"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", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
|
||||||
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), 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/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
|
||||||
r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index)
|
r.Get("/org/serviceaccounts", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index)
|
||||||
r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index)
|
r.Get("/org/serviceaccounts/:serviceAccountId", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index)
|
||||||
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
|
r.Get("/org/apikeys/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead)), hs.Index)
|
||||||
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
|
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
|
||||||
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin", reqGrafanaAdmin, hs.Index)
|
||||||
|
@ -149,8 +149,11 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
|
||||||
return (c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR)) &&
|
if !hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
||||||
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 {
|
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{
|
configNodes = append(configNodes, &dtos.NavLink{
|
||||||
Text: "API keys",
|
Text: "API keys",
|
||||||
Id: "apikeys",
|
Id: "apikeys",
|
||||||
|
@ -28,7 +28,10 @@ const ServiceAccountListItem = memo(
|
|||||||
const editUrl = `org/serviceaccounts/${serviceAccount.id}`;
|
const editUrl = `org/serviceaccounts/${serviceAccount.id}`;
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const canUpdateRole = contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount);
|
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 (
|
return (
|
||||||
<tr key={serviceAccount.id}>
|
<tr key={serviceAccount.id}>
|
||||||
@ -61,8 +64,9 @@ const ServiceAccountListItem = memo(
|
|||||||
{serviceAccount.login}
|
{serviceAccount.login}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td className={cx('link-td', styles.iconRow)}>
|
|
||||||
{contextSrv.licensedAccessControlEnabled() ? (
|
{contextSrv.licensedAccessControlEnabled() ? (
|
||||||
|
displayRolePicker && (
|
||||||
|
<td className={cx('link-td', styles.iconRow)}>
|
||||||
<UserRolePicker
|
<UserRolePicker
|
||||||
userId={serviceAccount.id}
|
userId={serviceAccount.id}
|
||||||
orgId={serviceAccount.orgId}
|
orgId={serviceAccount.orgId}
|
||||||
@ -70,17 +74,20 @@ const ServiceAccountListItem = memo(
|
|||||||
onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
||||||
roleOptions={roleOptions}
|
roleOptions={roleOptions}
|
||||||
builtInRoles={builtInRoles}
|
builtInRoles={builtInRoles}
|
||||||
disabled={rolePickerDisabled}
|
disabled={!enableRolePicker}
|
||||||
/>
|
/>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
|
<td className={cx('link-td', styles.iconRow)}>
|
||||||
<OrgRolePicker
|
<OrgRolePicker
|
||||||
aria-label="Role"
|
aria-label="Role"
|
||||||
value={serviceAccount.role}
|
value={serviceAccount.role}
|
||||||
disabled={!canUpdateRole}
|
disabled={!canUpdateRole}
|
||||||
onChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
onChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
<td className="link-td max-width-10">
|
<td className="link-td max-width-10">
|
||||||
<a
|
<a
|
||||||
className="ellipsis"
|
className="ellipsis"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ServiceAccountDTO, ThunkResult, ServiceAccountFilter } from '../../../types';
|
import { ServiceAccountDTO, ThunkResult, ServiceAccountFilter, AccessControlAction } from '../../../types';
|
||||||
import { getBackendSrv, locationService } from '@grafana/runtime';
|
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
acOptionsLoaded,
|
acOptionsLoaded,
|
||||||
@ -16,6 +16,7 @@ import {
|
|||||||
import { accessControlQueryParam } from 'app/core/utils/accessControl';
|
import { accessControlQueryParam } from 'app/core/utils/accessControl';
|
||||||
import { fetchBuiltinRoles, fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
import { fetchBuiltinRoles, fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { contextSrv } from '../../../core/services/context_srv';
|
||||||
import { ServiceAccountToken } from '../CreateServiceAccountTokenModal';
|
import { ServiceAccountToken } from '../CreateServiceAccountTokenModal';
|
||||||
|
|
||||||
const BASE_URL = `/api/serviceaccounts`;
|
const BASE_URL = `/api/serviceaccounts`;
|
||||||
@ -23,10 +24,17 @@ const BASE_URL = `/api/serviceaccounts`;
|
|||||||
export function fetchACOptions(): ThunkResult<void> {
|
export function fetchACOptions(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
|
if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
|
||||||
const options = await fetchRoleOptions();
|
const options = await fetchRoleOptions();
|
||||||
dispatch(acOptionsLoaded(options));
|
dispatch(acOptionsLoaded(options));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
contextSrv.licensedAccessControlEnabled() &&
|
||||||
|
contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)
|
||||||
|
) {
|
||||||
const builtInRoles = await fetchBuiltinRoles();
|
const builtInRoles = await fetchBuiltinRoles();
|
||||||
dispatch(builtInRolesLoaded(builtInRoles));
|
dispatch(builtInRolesLoaded(builtInRoles));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -195,14 +195,14 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/org/apikeys',
|
path: '/org/apikeys',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ActionAPIKeysRead]),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage')
|
() => import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/org/serviceaccounts',
|
path: '/org/serviceaccounts',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ServiceAccountsRead]),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "ServiceAccountsPage" */ 'app/features/serviceaccounts/ServiceAccountsListPage')
|
import(/* webpackChunkName: "ServiceAccountsPage" */ 'app/features/serviceaccounts/ServiceAccountsListPage')
|
||||||
|
@ -23,6 +23,7 @@ export enum AccessControlAction {
|
|||||||
UsersQuotasList = 'users.quotas:list',
|
UsersQuotasList = 'users.quotas:list',
|
||||||
UsersQuotasUpdate = 'users.quotas:update',
|
UsersQuotasUpdate = 'users.quotas:update',
|
||||||
|
|
||||||
|
ServiceAccountsRead = 'serviceaccounts:read',
|
||||||
ServiceAccountsCreate = 'serviceaccounts:create',
|
ServiceAccountsCreate = 'serviceaccounts:create',
|
||||||
ServiceAccountsWrite = 'serviceaccounts:write',
|
ServiceAccountsWrite = 'serviceaccounts:write',
|
||||||
ServiceAccountsDelete = 'serviceaccounts:delete',
|
ServiceAccountsDelete = 'serviceaccounts:delete',
|
||||||
@ -107,6 +108,8 @@ export enum AccessControlAction {
|
|||||||
// External alerting notifications actions.
|
// External alerting notifications actions.
|
||||||
AlertingNotificationsExternalWrite = 'alert.notifications.external:write',
|
AlertingNotificationsExternalWrite = 'alert.notifications.external:write',
|
||||||
AlertingNotificationsExternalRead = 'alert.notifications.external:read',
|
AlertingNotificationsExternalRead = 'alert.notifications.external:read',
|
||||||
|
|
||||||
|
ActionAPIKeysRead = 'apikeys:read',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
|
Loading…
Reference in New Issue
Block a user