grafana/pkg/services/accesscontrol/models.go
Gabriel MABILLE edf1775d49
AuthN: Embed an OAuth2 server for external service authentication (#68086)
* Moving POC files from #64283 to a new branch

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Adding missing permission definition

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Force the service instantiation while client isn't merged

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Merge conf with main

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Leave go-sqlite3 version unchanged

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* tidy

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* User SearchUserPermissions instead of SearchUsersPermissions

* Replace DummyKeyService with signingkeys.Service

* Use user🆔<id> as subject

* Fix introspection endpoint issue

* Add X-Grafana-Org-Id to get_resources.bash script

* Regenerate toggles_gen.go
* Fix basic.go

* Add GetExternalService tests

* Add GetPublicKeyScopes tests

* Add GetScopesOnUser tests

* Add GetScopes tests

* Add ParsePublicKeyPem tests

* Add database test for GetByName

* re-add comments

* client tests added

* Add GetExternalServicePublicKey tests

* Add other test case to GetExternalServicePublicKey

* client_credentials grant test

* Add test to jwtbearer grant

* Test Comments

* Add handleKeyOptions tests

* Add RSA key generation test

* Add ECDSA by default to EmbeddedSigningKeysService

* Clean up org id scope and audiences

* Add audiences to the DB

* Fix check on Audience

* Fix double import

* Add AC Store mock and align oauthserver tests

* Fix test after rebase

* Adding missing store function to mock

* Fix double import

* Add CODEOWNER

* Fix some linting errors

* errors don't need type assertion

* Typo codeowners

* use mockery for oauthserver store

* Add feature toggle check

* Fix db tests to handle the feature flag

* Adding call to DeleteExternalServiceRole

* Fix flaky test

* Re-organize routes comments and plan futur work

* Add client_id check to Extended JWT client

* Clean up

* Fix

* Remove background service registry instantiation of the OAuth server

* Comment cleanup

* Remove unused client function

* Update go.mod to use the latest ory/fosite commit

* Remove oauth2_server related configs from defaults.ini

* Add audiences to DTO

* Fix flaky test

* Remove registration endpoint and demo scripts. Document code

* Rename packages

* Remove the OAuthService vs OAuthServer confusion

* fix incorrect import ext_jwt_test

* Comments and order

* Comment basic auth

* Remove unecessary todo

* Clean api

* Moving ParsePublicKeyPem to utils

* re ordering functions in service.go

* Fix comment

* comment on the redirect uri

* Add RBAC actions, not only scopes

* Fix tests

* re-import featuremgmt in migrations

* Fix wire

* Fix scopes in test

* Fix flaky test

* Remove todo, the intersection should always return the minimal set

* Remove unecessary check from intersection code

* Allow env overrides on settings

* remove the term app name

* Remove app keyword for client instead and use Name instead of ExternalServiceName

* LogID remove ExternalService ref

* Use Name instead of ExternalServiceName

* Imports order

* Inline

* Using ExternalService and ExternalServiceDTO

* Remove xorm tags

* comment

* Rename client files

* client -> external service

* comments

* Move test to correct package

* slimmer test

* cachedUser -> cachedExternalService

* Fix aggregate store test

* PluginAuthSession -> AuthSession

* Revert the nil cehcks

* Remove unecessary extra

* Removing custom session

* fix typo in test

* Use constants for tests

* Simplify HandleToken tests

* Refactor the HandleTokenRequest test

* test message

* Review test

* Prevent flacky test on client as well

* go imports

* Revert changes from 526e48ad45

* AuthN: Change the External Service registration form (#68649)

* AuthN: change the External Service registration form

* Gen default permissions

* Change demo script registration form

* Remove unecessary comment

* Nit.

* Reduce cyclomatic complexity

* Remove demo_scripts

* Handle case with no service account

* Comments

* Group key gen

* Nit.

* Check the SaveExternalService test

* Rename cachedUser to cachedClient in test

* One more test case to database test

* Comments

* Remove last org scope

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

* Update pkg/services/oauthserver/utils/utils_test.go

* Update pkg/services/sqlstore/migrations/oauthserver/migrations.go

Remove comment

* Update pkg/setting/setting.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

---------

Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>
2023-05-25 15:38:30 +02:00

518 lines
15 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"
)
var ErrInternal = errutil.NewBase(errutil.StatusInternal, "accesscontrol.internal")
// 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(),
})
}
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)
}
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"`
Updated time.Time `json:"updated"`
Created time.Time `json:"created"`
}
func (p Permission) OSSPermission() Permission {
return Permission{
Action: p.Action,
Scope: p.Scope,
}
}
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
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 {
OrgID int64
Global bool
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)
if (cmd.OrgID == GlobalOrgID) != cmd.Global {
return fmt.Errorf("invalid org id %d for global role %t", cmd.OrgID, cmd.Global)
}
// 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
FixedRolePrefix = "fixed:"
ManagedRolePrefix = "managed:"
BasicRolePrefix = "basic:"
PluginRolePrefix = "plugins:"
ExternalServiceRolePrefix = "externalservice:"
BasicRoleUIDPrefix = "basic_"
ExternalServiceRoleUIDPrefix = "externalservice_"
RoleGrafanaAdmin = "Grafana Admin"
GeneralFolderUID = "general"
// 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:*"
ScopeSettingsAuth = "settings:auth:*"
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"
// 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"
ActionAlertingProvisioningWrite = "alert.provisioning:write"
)
var (
// Team scope
ScopeTeamsID = Scope("teams", "id", Parameter(":teamId"))
// 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)