grafana/pkg/services/accesscontrol/models.go
Yuri Tseretyan d1073deefd
Alerting: Time intervals API (read only endpoints) (#81672)
* 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
2024-02-01 15:17:13 -05:00

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)