Access control: expose permissions to the frontend (#32954)

* Expose user permissions to the frontend

* Do not include empty scope

* Extend ContextSrv with hasPermission() method

* Add access control types

* Fix type error (make permissions optional)

* Fallback if access control disabled

* Move UserPermission to types

* Simplify hasPermission()
This commit is contained in:
Alexander Zobnin 2021-04-16 16:02:16 +03:00 committed by GitHub
parent 6ae73eaa22
commit 8b843eb0a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 16 deletions

View File

@ -46,6 +46,7 @@ export interface FeatureToggles {
live: boolean; live: boolean;
ngalert: boolean; ngalert: boolean;
panelLibrary: boolean; panelLibrary: boolean;
accesscontrol: boolean;
/** /**
* @remarks * @remarks

View File

@ -57,6 +57,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
ngalert: false, ngalert: false,
panelLibrary: false, panelLibrary: false,
reportVariables: false, reportVariables: false,
accesscontrol: false,
}; };
licenseInfo: LicenseInfo = {} as LicenseInfo; licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false; rendererAvailable = false;

View File

@ -25,24 +25,27 @@ type LoginCommand struct {
} }
type CurrentUser struct { type CurrentUser struct {
IsSignedIn bool `json:"isSignedIn"` IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"` Id int64 `json:"id"`
Login string `json:"login"` Login string `json:"login"`
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` Name string `json:"name"`
LightTheme bool `json:"lightTheme"` LightTheme bool `json:"lightTheme"`
OrgCount int `json:"orgCount"` OrgCount int `json:"orgCount"`
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"` OrgName string `json:"orgName"`
OrgRole models.RoleType `json:"orgRole"` OrgRole models.RoleType `json:"orgRole"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"` GravatarUrl string `json:"gravatarUrl"`
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
Locale string `json:"locale"` Locale string `json:"locale"`
HelpFlags1 models.HelpFlags1 `json:"helpFlags1"` HelpFlags1 models.HelpFlags1 `json:"helpFlags1"`
HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"` HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"`
Permissions UserPermissionsMap `json:"permissions,omitempty"`
} }
type UserPermissionsMap map[string]map[string]string
type MetricRequest struct { type MetricRequest struct {
From string `json:"from"` From string `json:"from"`
To string `json:"to"` To string `json:"to"`

View File

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -427,6 +428,15 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()), ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()),
} }
if hs.Cfg.FeatureToggles["accesscontrol"] {
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
if err != nil {
return nil, err
}
data.User.Permissions = accesscontrol.BuildPermissionsMap(userPermissions)
}
if setting.DisableGravatar { if setting.DisableGravatar {
data.User.GravatarUrl = hs.Cfg.AppSubURL + "/public/img/user_profile.png" data.User.GravatarUrl = hs.Cfg.AppSubURL + "/public/img/user_profile.png"
} }

View File

@ -16,3 +16,22 @@ type AccessControl interface {
// Middleware checks if service disabled or not to switch to fallback authorization. // Middleware checks if service disabled or not to switch to fallback authorization.
IsDisabled() bool IsDisabled() bool
} }
func BuildPermissionsMap(permissions []*Permission) map[string]map[string]string {
permissionsMap := make(map[string]map[string]string)
for _, p := range permissions {
if item, ok := permissionsMap[p.Action]; ok {
if _, ok := item[p.Scope]; !ok && p.Scope != "" {
permissionsMap[p.Action][p.Scope] = p.Scope
}
} else {
newItem := make(map[string]string)
if p.Scope != "" {
newItem[p.Scope] = p.Scope
}
permissionsMap[p.Action] = newItem
}
}
return permissionsMap
}

View File

@ -2,6 +2,7 @@ import config from '../../core/config';
import _ from 'lodash'; import _ from 'lodash';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { rangeUtil } from '@grafana/data'; import { rangeUtil } from '@grafana/data';
import { AccessControlAction, AccessControlScope, UserPermission } from 'app/types';
export class User { export class User {
id: number; id: number;
@ -17,6 +18,7 @@ export class User {
lightTheme: boolean; lightTheme: boolean;
hasEditPermissionInFolders: boolean; hasEditPermissionInFolders: boolean;
email?: string; email?: string;
permissions?: UserPermission;
constructor() { constructor() {
this.id = 0; this.id = 0;
@ -74,6 +76,16 @@ export class ContextSrv {
return this.user.orgRole === role; return this.user.orgRole === role;
} }
// Checks whether user has required permission
hasPermission(action: AccessControlAction, scope?: AccessControlScope): boolean {
// Fallback if access control disabled
if (!config.featureToggles['accesscontrol']) {
return true;
}
return !!(this.user.permissions?.[action] && (scope ? this.user.permissions[action][scope] : true));
}
isGrafanaVisible() { isGrafanaVisible() {
return !!(document.visibilityState === undefined || document.visibilityState === 'visible'); return !!(document.visibilityState === undefined || document.visibilityState === 'visible');
} }

View File

@ -0,0 +1,38 @@
/**
* UserPermission is a map storing permissions in a form of
* {
* action: { scope: scope }
* }
*/
export type UserPermission = {
[key: string]: { [key: string]: string };
};
export interface AccessControlPermission {
action: AccessControlAction;
scope?: AccessControlScope;
}
// Permission actions
export enum AccessControlAction {
UsersRead = 'users:read',
UsersWrite = 'users:write',
UsersTeamRead = 'users.teams:read',
UsersAuthTokenList = 'users.authtoken:list',
UsersAuthTokenUpdate = 'users.authtoken:update',
UsersPasswordUpdate = 'users.password.update',
UsersDelete = 'users:delete',
UsersCreate = 'users:create',
UsersEnable = 'users:enable',
UsersDisable = 'users:disable',
UsersPermissionsUpdate = 'users.permissions.update',
UsersLogout = 'users:logout',
UsersQuotasList = 'users.quotas:list',
UsersQuotasUpdate = 'users.quotas:update',
}
// Global Scopes
export enum AccessControlScope {
UsersAll = 'users:*',
UsersSelf = 'users:self',
}

View File

@ -17,6 +17,7 @@ export * from './appEvent';
export * from './angular'; export * from './angular';
export * from './query'; export * from './query';
export * from './preferences'; export * from './preferences';
export * from './accessControl';
import * as CoreEvents from './events'; import * as CoreEvents from './events';
export { CoreEvents }; export { CoreEvents };