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;
ngalert: boolean;
panelLibrary: boolean;
accesscontrol: boolean;
/**
* @remarks

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"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()),
}
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 {
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.
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 coreModule from 'app/core/core_module';
import { rangeUtil } from '@grafana/data';
import { AccessControlAction, AccessControlScope, UserPermission } from 'app/types';
export class User {
id: number;
@ -17,6 +18,7 @@ export class User {
lightTheme: boolean;
hasEditPermissionInFolders: boolean;
email?: string;
permissions?: UserPermission;
constructor() {
this.id = 0;
@ -74,6 +76,16 @@ export class ContextSrv {
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() {
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 './query';
export * from './preferences';
export * from './accessControl';
import * as CoreEvents from './events';
export { CoreEvents };