diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 063afc00c75..9dae23642a3 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -46,6 +46,7 @@ export interface FeatureToggles { live: boolean; ngalert: boolean; panelLibrary: boolean; + accesscontrol: boolean; /** * @remarks diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index d88e059a43b..57221ffcf7e 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -57,6 +57,7 @@ export class GrafanaBootConfig implements GrafanaConfig { ngalert: false, panelLibrary: false, reportVariables: false, + accesscontrol: false, }; licenseInfo: LicenseInfo = {} as LicenseInfo; rendererAvailable = false; diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index a884627b8e6..91e27c8edce 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -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"` diff --git a/pkg/api/index.go b/pkg/api/index.go index 84b5e41d90b..1c2b0f50702 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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" } diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 9885b47f7cd..da5c67605e9 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -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 +} diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 23532842c17..243c7bb69da 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -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'); } diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts new file mode 100644 index 00000000000..0be7ff49b8a --- /dev/null +++ b/public/app/types/accessControl.ts @@ -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', +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index a6c51a6c388..1a566ab71b1 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -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 };