mirror of
https://github.com/grafana/grafana.git
synced 2024-12-02 05:29:42 -06:00
d1073deefd
* declare new API and models GettableTimeIntervals, PostableTimeIntervals * add new actions alert.notifications.time-intervals:read and alert.notifications.time-intervals:write. * update existing alerting roles with the read action. Add to all alerting roles. * add integration tests
561 lines
16 KiB
Go
561 lines
16 KiB
Go
package accesscontrol
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/slugify"
|
|
"github.com/grafana/grafana/pkg/services/annotations"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/util/errutil"
|
|
)
|
|
|
|
const (
|
|
CacheHit = "hit"
|
|
CacheMiss = "miss"
|
|
)
|
|
|
|
var (
|
|
ErrInternal = errutil.Internal("accesscontrol.internal")
|
|
CacheUsageStatuses = []string{CacheHit, CacheMiss}
|
|
)
|
|
|
|
// RoleRegistration stores a role and its assignments to built-in roles
|
|
// (Viewer, Editor, Admin, Grafana Admin)
|
|
type RoleRegistration struct {
|
|
Role RoleDTO
|
|
Grants []string
|
|
}
|
|
|
|
// Role is the model for Role in RBAC.
|
|
type Role struct {
|
|
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
|
|
OrgID int64 `json:"-" xorm:"org_id"`
|
|
Version int64 `json:"version"`
|
|
UID string `xorm:"uid" json:"uid"`
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"displayName,omitempty"`
|
|
Group string `xorm:"group_name" json:"group"`
|
|
Description string `json:"description"`
|
|
Hidden bool `json:"hidden"`
|
|
|
|
Updated time.Time `json:"updated"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
func (r *Role) Global() bool {
|
|
return r.OrgID == GlobalOrgID
|
|
}
|
|
|
|
func (r *Role) IsFixed() bool {
|
|
return strings.HasPrefix(r.Name, FixedRolePrefix)
|
|
}
|
|
|
|
func (r *Role) IsBasic() bool {
|
|
return strings.HasPrefix(r.Name, BasicRolePrefix) || strings.HasPrefix(r.UID, BasicRoleUIDPrefix)
|
|
}
|
|
|
|
func (r Role) MarshalJSON() ([]byte, error) {
|
|
type Alias Role
|
|
|
|
return json.Marshal(&struct {
|
|
Alias
|
|
Global bool `json:"global" xorm:"-"`
|
|
}{
|
|
Alias: (Alias)(r),
|
|
Global: r.Global(),
|
|
})
|
|
}
|
|
|
|
// swagger:ignore
|
|
type RoleDTO struct {
|
|
Version int64 `json:"version"`
|
|
UID string `xorm:"uid" json:"uid"`
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"displayName,omitempty"`
|
|
Description string `json:"description"`
|
|
Group string `xorm:"group_name" json:"group"`
|
|
Permissions []Permission `json:"permissions,omitempty"`
|
|
Delegatable *bool `json:"delegatable,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
|
|
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
|
|
OrgID int64 `json:"-" xorm:"org_id"`
|
|
|
|
Updated time.Time `json:"updated"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
func (r *RoleDTO) LogID() string {
|
|
var org string
|
|
|
|
if r.Global() {
|
|
org = "Global"
|
|
} else {
|
|
org = fmt.Sprintf("OrgId:%v", r.OrgID)
|
|
}
|
|
|
|
if r.UID != "" {
|
|
return fmt.Sprintf("[%s RoleUID:%v]", org, r.UID)
|
|
}
|
|
return fmt.Sprintf("[%s Role:%v]", org, r.Name)
|
|
}
|
|
|
|
func (r *RoleDTO) Role() Role {
|
|
return Role{
|
|
ID: r.ID,
|
|
OrgID: r.OrgID,
|
|
UID: r.UID,
|
|
Version: r.Version,
|
|
Name: r.Name,
|
|
DisplayName: r.DisplayName,
|
|
Group: r.Group,
|
|
Description: r.Description,
|
|
Hidden: r.Hidden,
|
|
Updated: r.Updated,
|
|
Created: r.Created,
|
|
}
|
|
}
|
|
|
|
func (r *RoleDTO) Global() bool {
|
|
return r.OrgID == GlobalOrgID
|
|
}
|
|
|
|
func (r *RoleDTO) IsManaged() bool {
|
|
return strings.HasPrefix(r.Name, ManagedRolePrefix)
|
|
}
|
|
|
|
func (r *RoleDTO) IsFixed() bool {
|
|
return strings.HasPrefix(r.Name, FixedRolePrefix)
|
|
}
|
|
|
|
func (r *RoleDTO) IsPlugin() bool {
|
|
return strings.HasPrefix(r.Name, PluginRolePrefix)
|
|
}
|
|
|
|
func (r *RoleDTO) IsBasic() bool {
|
|
return strings.HasPrefix(r.Name, BasicRolePrefix) || strings.HasPrefix(r.UID, BasicRoleUIDPrefix)
|
|
}
|
|
|
|
func (r *RoleDTO) IsExternalService() bool {
|
|
return strings.HasPrefix(r.Name, ExternalServiceRolePrefix) || strings.HasPrefix(r.UID, ExternalServiceRoleUIDPrefix)
|
|
}
|
|
|
|
// swagger:model RoleDTO
|
|
type RoleDTOStatic struct {
|
|
RoleDTO
|
|
Global bool `json:"global" xorm:"-"`
|
|
}
|
|
|
|
func (r RoleDTO) MarshalJSON() ([]byte, error) {
|
|
type Alias RoleDTO
|
|
|
|
return json.Marshal(&struct {
|
|
Alias
|
|
Global bool `json:"global" xorm:"-"`
|
|
}{
|
|
Alias: (Alias)(r),
|
|
Global: r.Global(),
|
|
})
|
|
}
|
|
|
|
type TeamRole struct {
|
|
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
|
OrgID int64 `json:"orgId" xorm:"org_id"`
|
|
RoleID int64 `json:"roleId" xorm:"role_id"`
|
|
TeamID int64 `json:"teamId" xorm:"team_id"`
|
|
|
|
Created time.Time
|
|
}
|
|
|
|
type UserRole struct {
|
|
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
|
OrgID int64 `json:"orgId" xorm:"org_id"`
|
|
RoleID int64 `json:"roleId" xorm:"role_id"`
|
|
UserID int64 `json:"userId" xorm:"user_id"`
|
|
|
|
Created time.Time
|
|
}
|
|
|
|
type BuiltinRole struct {
|
|
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
|
RoleID int64 `json:"roleId" xorm:"role_id"`
|
|
OrgID int64 `json:"orgId" xorm:"org_id"`
|
|
Role string
|
|
|
|
Updated time.Time
|
|
Created time.Time
|
|
}
|
|
|
|
// Permission is the model for access control permissions.
|
|
type Permission struct {
|
|
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
|
|
RoleID int64 `json:"-" xorm:"role_id"`
|
|
Action string `json:"action"`
|
|
Scope string `json:"scope"`
|
|
|
|
Kind string `json:"-"`
|
|
Attribute string `json:"-"`
|
|
Identifier string `json:"-"`
|
|
|
|
Updated time.Time `json:"updated"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
func (p Permission) OSSPermission() Permission {
|
|
return Permission{
|
|
Action: p.Action,
|
|
Scope: p.Scope,
|
|
}
|
|
}
|
|
|
|
// SplitScope returns kind, attribute and Identifier
|
|
func (p Permission) SplitScope() (string, string, string) {
|
|
if p.Scope == "" {
|
|
return "", "", ""
|
|
}
|
|
|
|
fragments := strings.Split(p.Scope, ":")
|
|
switch l := len(fragments); l {
|
|
case 1: // Splitting a wildcard scope "*" -> kind: "*"; attribute: "*"; identifier: "*"
|
|
return fragments[0], fragments[0], fragments[0]
|
|
case 2: // Splitting a wildcard scope with specified kind "dashboards:*" -> kind: "dashboards"; attribute: "*"; identifier: "*"
|
|
return fragments[0], fragments[1], fragments[1]
|
|
default: // Splitting a scope with all fields specified "dashboards:uid:my_dash" -> kind: "dashboards"; attribute: "uid"; identifier: "my_dash"
|
|
return fragments[0], fragments[1], strings.Join(fragments[2:], ":")
|
|
}
|
|
}
|
|
|
|
type GetUserPermissionsQuery struct {
|
|
OrgID int64
|
|
UserID int64
|
|
Roles []string
|
|
TeamIDs []int64
|
|
RolePrefixes []string
|
|
}
|
|
|
|
// ResourcePermission is structure that holds all actions that either a team / user / builtin-role
|
|
// can perform against specific resource.
|
|
type ResourcePermission struct {
|
|
ID int64
|
|
RoleName string
|
|
Actions []string
|
|
Scope string
|
|
UserId int64
|
|
UserLogin string
|
|
UserEmail string
|
|
TeamId int64
|
|
TeamEmail string
|
|
Team string
|
|
BuiltInRole string
|
|
IsManaged bool
|
|
IsInherited bool
|
|
IsServiceAccount bool
|
|
Created time.Time
|
|
Updated time.Time
|
|
}
|
|
|
|
func (p *ResourcePermission) Contains(targetActions []string) bool {
|
|
if len(p.Actions) < len(targetActions) {
|
|
return false
|
|
}
|
|
|
|
var contain = func(arr []string, s string) bool {
|
|
for _, item := range arr {
|
|
if item == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
for _, a := range targetActions {
|
|
if !contain(p.Actions, a) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type SetResourcePermissionCommand struct {
|
|
UserID int64 `json:"userId,omitempty"`
|
|
TeamID int64 `json:"teamId,omitempty"`
|
|
BuiltinRole string `json:"builtInRole,omitempty"`
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
type SaveExternalServiceRoleCommand struct {
|
|
AssignmentOrgID int64
|
|
ExternalServiceID string
|
|
ServiceAccountID int64
|
|
Permissions []Permission
|
|
}
|
|
|
|
func (cmd *SaveExternalServiceRoleCommand) Validate() error {
|
|
if cmd.ExternalServiceID == "" {
|
|
return errors.New("external service id not specified")
|
|
}
|
|
|
|
// slugify the external service id ID for the role to have correct name and uid
|
|
cmd.ExternalServiceID = slugify.Slugify(cmd.ExternalServiceID)
|
|
|
|
// Check and deduplicate permissions
|
|
if cmd.Permissions == nil || len(cmd.Permissions) == 0 {
|
|
return errors.New("no permissions provided")
|
|
}
|
|
dedupMap := map[Permission]bool{}
|
|
dedup := make([]Permission, 0, len(cmd.Permissions))
|
|
for i := range cmd.Permissions {
|
|
if len(cmd.Permissions[i].Action) == 0 {
|
|
return fmt.Errorf("external service %v requests a permission with no Action", cmd.ExternalServiceID)
|
|
}
|
|
if dedupMap[cmd.Permissions[i]] {
|
|
continue
|
|
}
|
|
dedupMap[cmd.Permissions[i]] = true
|
|
dedup = append(dedup, cmd.Permissions[i])
|
|
}
|
|
cmd.Permissions = dedup
|
|
|
|
if cmd.ServiceAccountID <= 0 {
|
|
return fmt.Errorf("invalid service account id %d", cmd.ServiceAccountID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
GlobalOrgID = 0
|
|
NoOrgID = int64(-1)
|
|
GeneralFolderUID = "general"
|
|
RoleGrafanaAdmin = "Grafana Admin"
|
|
|
|
// Permission actions
|
|
|
|
ActionAPIKeyRead = "apikeys:read"
|
|
ActionAPIKeyCreate = "apikeys:create"
|
|
ActionAPIKeyDelete = "apikeys:delete"
|
|
|
|
// Users actions
|
|
ActionUsersRead = "users:read"
|
|
ActionUsersWrite = "users:write"
|
|
ActionUsersImpersonate = "users:impersonate"
|
|
|
|
// We can ignore gosec G101 since this does not contain any credentials.
|
|
// nolint:gosec
|
|
ActionUsersAuthTokenList = "users.authtoken:read"
|
|
// We can ignore gosec G101 since this does not contain any credentials.
|
|
// nolint:gosec
|
|
ActionUsersAuthTokenUpdate = "users.authtoken:write"
|
|
// We can ignore gosec G101 since this does not contain any credentials.
|
|
// nolint:gosec
|
|
ActionUsersPasswordUpdate = "users.password:write"
|
|
ActionUsersDelete = "users:delete"
|
|
ActionUsersCreate = "users:create"
|
|
ActionUsersEnable = "users:enable"
|
|
ActionUsersDisable = "users:disable"
|
|
ActionUsersPermissionsUpdate = "users.permissions:write"
|
|
ActionUsersLogout = "users:logout"
|
|
ActionUsersQuotasList = "users.quotas:read"
|
|
ActionUsersQuotasUpdate = "users.quotas:write"
|
|
ActionUsersPermissionsRead = "users.permissions:read"
|
|
|
|
// Org actions
|
|
ActionOrgsRead = "orgs:read"
|
|
ActionOrgsPreferencesRead = "orgs.preferences:read"
|
|
ActionOrgsQuotasRead = "orgs.quotas:read"
|
|
ActionOrgsWrite = "orgs:write"
|
|
ActionOrgsPreferencesWrite = "orgs.preferences:write"
|
|
ActionOrgsQuotasWrite = "orgs.quotas:write"
|
|
ActionOrgsDelete = "orgs:delete"
|
|
ActionOrgsCreate = "orgs:create"
|
|
|
|
ActionOrgUsersRead = "org.users:read"
|
|
ActionOrgUsersAdd = "org.users:add"
|
|
ActionOrgUsersRemove = "org.users:remove"
|
|
ActionOrgUsersWrite = "org.users:write"
|
|
|
|
// LDAP actions
|
|
ActionLDAPUsersRead = "ldap.user:read"
|
|
ActionLDAPUsersSync = "ldap.user:sync"
|
|
ActionLDAPStatusRead = "ldap.status:read"
|
|
ActionLDAPConfigReload = "ldap.config:reload"
|
|
|
|
// Server actions
|
|
ActionServerStatsRead = "server.stats:read"
|
|
|
|
// Settings actions
|
|
ActionSettingsRead = "settings:read"
|
|
ActionSettingsWrite = "settings:write"
|
|
|
|
// Datasources actions
|
|
ActionDatasourcesExplore = "datasources:explore"
|
|
|
|
// Global Scopes
|
|
ScopeGlobalUsersAll = "global.users:*"
|
|
|
|
// APIKeys scope
|
|
ScopeAPIKeysAll = "apikeys:*"
|
|
|
|
// Users scope
|
|
ScopeUsersAll = "users:*"
|
|
ScopeUsersPrefix = "users:id:"
|
|
|
|
// Settings scope
|
|
ScopeSettingsAll = "settings:*"
|
|
ScopeSettingsSAML = "settings:auth.saml:*"
|
|
|
|
// Team related actions
|
|
ActionTeamsCreate = "teams:create"
|
|
ActionTeamsDelete = "teams:delete"
|
|
ActionTeamsRead = "teams:read"
|
|
ActionTeamsWrite = "teams:write"
|
|
ActionTeamsPermissionsRead = "teams.permissions:read"
|
|
ActionTeamsPermissionsWrite = "teams.permissions:write"
|
|
|
|
// Team related scopes
|
|
ScopeTeamsAll = "teams:*"
|
|
|
|
// Annotations related actions
|
|
ActionAnnotationsCreate = "annotations:create"
|
|
ActionAnnotationsDelete = "annotations:delete"
|
|
ActionAnnotationsRead = "annotations:read"
|
|
ActionAnnotationsWrite = "annotations:write"
|
|
|
|
// Alert scopes are divided into two groups. The internal (to Grafana) and the external ones.
|
|
// For the Grafana ones, given we have ACID control we're able to provide better granularity by defining CRUD options.
|
|
// For the external ones, we only have read and write permissions due to the lack of atomicity control of the external system.
|
|
|
|
// Alerting rules actions
|
|
ActionAlertingRuleCreate = "alert.rules:create"
|
|
ActionAlertingRuleRead = "alert.rules:read"
|
|
ActionAlertingRuleUpdate = "alert.rules:write"
|
|
ActionAlertingRuleDelete = "alert.rules:delete"
|
|
|
|
// Alerting instances (+silences) actions
|
|
ActionAlertingInstanceCreate = "alert.instances:create"
|
|
ActionAlertingInstanceUpdate = "alert.instances:write"
|
|
ActionAlertingInstanceRead = "alert.instances:read"
|
|
|
|
// Alerting Notification policies actions
|
|
ActionAlertingNotificationsRead = "alert.notifications:read"
|
|
ActionAlertingNotificationsWrite = "alert.notifications:write"
|
|
|
|
// Alerting notifications time interval actions
|
|
ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read"
|
|
ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write"
|
|
|
|
// External alerting rule actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system.
|
|
ActionAlertingRuleExternalWrite = "alert.rules.external:write"
|
|
ActionAlertingRuleExternalRead = "alert.rules.external:read"
|
|
|
|
// External alerting instances actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system.
|
|
ActionAlertingInstancesExternalWrite = "alert.instances.external:write"
|
|
ActionAlertingInstancesExternalRead = "alert.instances.external:read"
|
|
|
|
// External alerting notifications actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system.
|
|
ActionAlertingNotificationsExternalWrite = "alert.notifications.external:write"
|
|
ActionAlertingNotificationsExternalRead = "alert.notifications.external:read"
|
|
|
|
// Alerting provisioning actions
|
|
ActionAlertingProvisioningRead = "alert.provisioning:read"
|
|
ActionAlertingProvisioningReadSecrets = "alert.provisioning.secrets:read"
|
|
ActionAlertingProvisioningWrite = "alert.provisioning:write"
|
|
|
|
// Feature Management actions
|
|
ActionFeatureManagementRead = "featuremgmt.read"
|
|
ActionFeatureManagementWrite = "featuremgmt.write"
|
|
|
|
// Library Panel actions
|
|
ActionLibraryPanelsCreate = "library.panels:create"
|
|
ActionLibraryPanelsRead = "library.panels:read"
|
|
ActionLibraryPanelsWrite = "library.panels:write"
|
|
ActionLibraryPanelsDelete = "library.panels:delete"
|
|
)
|
|
|
|
var (
|
|
// Team scope
|
|
ScopeTeamsID = Scope("teams", "id", Parameter(":teamId"))
|
|
|
|
ScopeSettingsOAuth = func(provider string) string {
|
|
return Scope("settings", "auth."+provider, "*")
|
|
}
|
|
|
|
// Annotation scopes
|
|
ScopeAnnotationsRoot = "annotations"
|
|
ScopeAnnotationsProvider = NewScopeProvider(ScopeAnnotationsRoot)
|
|
ScopeAnnotationsAll = ScopeAnnotationsProvider.GetResourceAllScope()
|
|
ScopeAnnotationsID = Scope(ScopeAnnotationsRoot, "id", Parameter(":annotationId"))
|
|
ScopeAnnotationsTypeDashboard = ScopeAnnotationsProvider.GetResourceScopeType(annotations.Dashboard.String())
|
|
ScopeAnnotationsTypeOrganization = ScopeAnnotationsProvider.GetResourceScopeType(annotations.Organization.String())
|
|
)
|
|
|
|
func BuiltInRolesWithParents(builtInRoles []string) map[string]struct{} {
|
|
res := map[string]struct{}{}
|
|
|
|
for _, br := range builtInRoles {
|
|
res[br] = struct{}{}
|
|
if br != RoleGrafanaAdmin {
|
|
for _, parent := range org.RoleType(br).Parents() {
|
|
res[string(parent)] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// Evaluators
|
|
|
|
// TeamsAccessEvaluator is used to protect the "Configuration > Teams" page access
|
|
// grants access to a user when they can either create teams or can read and update a team
|
|
var TeamsAccessEvaluator = EvalAny(
|
|
EvalPermission(ActionTeamsCreate),
|
|
EvalAll(
|
|
EvalPermission(ActionTeamsRead),
|
|
EvalAny(
|
|
EvalPermission(ActionTeamsWrite),
|
|
EvalPermission(ActionTeamsPermissionsWrite),
|
|
),
|
|
),
|
|
)
|
|
|
|
// TeamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access
|
|
var TeamsEditAccessEvaluator = EvalAll(
|
|
EvalPermission(ActionTeamsRead),
|
|
EvalAny(
|
|
EvalPermission(ActionTeamsCreate),
|
|
EvalPermission(ActionTeamsWrite),
|
|
EvalPermission(ActionTeamsPermissionsWrite),
|
|
),
|
|
)
|
|
|
|
// OrgPreferencesAccessEvaluator is used to protect the "Configure > Preferences" page access
|
|
var OrgPreferencesAccessEvaluator = EvalAny(
|
|
EvalAll(
|
|
EvalPermission(ActionOrgsRead),
|
|
EvalPermission(ActionOrgsWrite),
|
|
),
|
|
EvalAll(
|
|
EvalPermission(ActionOrgsPreferencesRead),
|
|
EvalPermission(ActionOrgsPreferencesWrite),
|
|
),
|
|
)
|
|
|
|
// OrgsAccessEvaluator is used to protect the "Server Admin > Orgs" page access
|
|
// (you need to have read access to update or delete orgs; read is the minimum)
|
|
var OrgsAccessEvaluator = EvalPermission(ActionOrgsRead)
|
|
|
|
// OrgsCreateAccessEvaluator is used to protect the "Server Admin > Orgs > New Org" page access
|
|
var OrgsCreateAccessEvaluator = EvalAll(
|
|
EvalPermission(ActionOrgsRead),
|
|
EvalPermission(ActionOrgsCreate),
|
|
)
|
|
|
|
// ApiKeyAccessEvaluator is used to protect the "Configuration > API keys" page access
|
|
var ApiKeyAccessEvaluator = EvalPermission(ActionAPIKeyRead)
|