package accesscontrol import ( "context" "errors" "fmt" "strconv" "strings" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" ) type AccessControl interface { // Evaluate evaluates access to the given resources. Evaluate(ctx context.Context, user identity.Requester, evaluator Evaluator) (bool, error) // RegisterScopeAttributeResolver allows the caller to register a scope resolver for a // specific scope prefix (ex: datasources:name:) RegisterScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) } type Service interface { registry.ProvidesUsageStats // GetRoleByName returns a role by name GetRoleByName(ctx context.Context, orgID int64, roleName string) (*RoleDTO, error) // GetUserPermissions returns user permissions with only action and scope fields set. GetUserPermissions(ctx context.Context, user identity.Requester, options Options) ([]Permission, error) // SearchUsersPermissions returns all users' permissions filtered by an action prefix SearchUsersPermissions(ctx context.Context, user identity.Requester, options SearchOptions) (map[int64][]Permission, error) // ClearUserPermissionCache removes the permission cache entry for the given user ClearUserPermissionCache(user identity.Requester) // SearchUserPermissions returns single user's permissions filtered by an action prefix or an action SearchUserPermissions(ctx context.Context, orgID int64, filterOptions SearchOptions) ([]Permission, error) // DeleteUserPermissions removes all permissions user has in org and all permission to that user // If orgID is set to 0 remove permissions from all orgs DeleteUserPermissions(ctx context.Context, orgID, userID int64) error // DeleteTeamPermissions removes all role assignments and permissions granted to a team // and removes permissions scoped to the team. DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error // DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their // assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" DeclareFixedRoles(registrations ...RoleRegistration) error // SaveExternalServiceRole creates or updates an external service's role and assigns it to a given service account id. SaveExternalServiceRole(ctx context.Context, cmd SaveExternalServiceRoleCommand) error // DeleteExternalServiceRole removes an external service's role and its assignment. DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error // SyncUserRoles adds provided roles to user SyncUserRoles(ctx context.Context, orgID int64, cmd SyncUserRolesCommand) error } //go:generate mockery --name Store --structname MockStore --outpkg actest --filename store_mock.go --output ./actest/ type Store interface { GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]Permission, error) SearchUsersPermissions(ctx context.Context, orgID int64, options SearchOptions) (map[int64][]Permission, error) GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error SaveExternalServiceRole(ctx context.Context, cmd SaveExternalServiceRoleCommand) error DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error } type RoleRegistry interface { // RegisterFixedRoles registers all roles declared to AccessControl RegisterFixedRoles(ctx context.Context) error } type Options struct { ReloadCache bool } type SearchOptions struct { ActionPrefix string // Needed for the PoC v1, it's probably going to be removed. Action string Scope string NamespacedID string // ID of the identity (ex: user:3, service-account:4) wildcards Wildcards // private field computed based on the Scope RolePrefixes []string } // Wildcards computes the wildcard scopes that include the scope func (s *SearchOptions) Wildcards() []string { if s.wildcards != nil { return s.wildcards } if s.Scope == "" { s.wildcards = []string{} return s.wildcards } s.wildcards = WildcardsFromPrefix(ScopePrefix(s.Scope)) return s.wildcards } func (s *SearchOptions) ComputeUserID() (int64, error) { if s.NamespacedID == "" { return 0, errors.New("namespacedID must be set") } // Split namespaceID into namespace and ID parts := strings.Split(s.NamespacedID, ":") // Validate namespace ID format if len(parts) != 2 { return 0, fmt.Errorf("invalid namespaced ID: %s", s.NamespacedID) } // Validate namespace type is user or service account if parts[0] != identity.NamespaceUser && parts[0] != identity.NamespaceServiceAccount { return 0, fmt.Errorf("invalid namespace: %s", parts[0]) } // Validate namespace ID is a number id, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { return 0, fmt.Errorf("invalid namespaced ID: %s", s.NamespacedID) } return id, nil } type SyncUserRolesCommand struct { UserID int64 // name of roles the user should have RolesToAdd []string // name of roles the user should not have RolesToRemove []string } type TeamPermissionsService interface { GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]ResourcePermission, error) SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error) } type FolderPermissionsService interface { PermissionsService } type DashboardPermissionsService interface { PermissionsService } type DatasourcePermissionsService interface { PermissionsService } type ServiceAccountPermissionsService interface { PermissionsService } type PermissionsService interface { // GetPermissions returns all permissions for given resourceID GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]ResourcePermission, error) // SetUserPermission sets permission on resource for a user SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error) // SetTeamPermission sets permission on resource for a team SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*ResourcePermission, error) // SetBuiltInRolePermission sets permission on resource for a built-in role (Admin, Editor, Viewer) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*ResourcePermission, error) // SetPermissions sets several permissions on resource for either built-in role, team or user SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...SetResourcePermissionCommand) ([]ResourcePermission, error) // MapActions will map actions for a ResourcePermissions to it's "friendly" name configured in PermissionsToActions map. MapActions(permission ResourcePermission) string // DeleteResourcePermissions removes all permissions for a resource DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error } type User struct { ID int64 IsExternal bool } // HasGlobalAccess checks user access with globally assigned permissions only func HasGlobalAccess(ac AccessControl, authnService authn.Service, c *contextmodel.ReqContext) func(evaluator Evaluator) bool { return func(evaluator Evaluator) bool { var targetOrgID int64 = GlobalOrgID orgUser, err := authnService.ResolveIdentity(c.Req.Context(), targetOrgID, c.SignedInUser.GetID()) if err != nil { deny(c, nil, fmt.Errorf("failed to authenticate user in target org: %w", err)) } hasAccess, err := ac.Evaluate(c.Req.Context(), orgUser, evaluator) if err != nil { c.Logger.Error("Error from access control system", "error", err) return false } // guard against nil map if c.SignedInUser.Permissions == nil { c.SignedInUser.Permissions = make(map[int64]map[string][]string) } // set on user so we don't fetch global permissions every time this is called c.SignedInUser.Permissions[orgUser.GetOrgID()] = orgUser.GetPermissions() return hasAccess } } func HasAccess(ac AccessControl, c *contextmodel.ReqContext) func(evaluator Evaluator) bool { return func(evaluator Evaluator) bool { hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, evaluator) if err != nil { c.Logger.Error("Error from access control system", "error", err) return false } return hasAccess } } var ReqSignedIn = func(c *contextmodel.ReqContext) bool { return c.IsSignedIn } var ReqGrafanaAdmin = func(c *contextmodel.ReqContext) bool { return c.SignedInUser.GetIsGrafanaAdmin() } // ReqHasRole generates a fallback to check whether the user has a role // ReqHasRole(org.RoleAdmin) will always return true for Grafana server admins, eg, a Grafana Admin / Viewer role combination func ReqHasRole(role org.RoleType) func(c *contextmodel.ReqContext) bool { return func(c *contextmodel.ReqContext) bool { return c.SignedInUser.HasRole(role) } } func BuildPermissionsMap(permissions []Permission) map[string]bool { permissionsMap := make(map[string]bool) for _, p := range permissions { permissionsMap[p.Action] = true } return permissionsMap } // GroupScopesByAction will group scopes on action func GroupScopesByAction(permissions []Permission) map[string][]string { m := make(map[string][]string) for i := range permissions { m[permissions[i].Action] = append(m[permissions[i].Action], permissions[i].Scope) } return m } // Reduce will reduce a list of permissions to its minimal form, grouping scopes by action func Reduce(ps []Permission) map[string][]string { reduced := make(map[string][]string) scopesByAction := make(map[string]map[string]bool) wildcardsByAction := make(map[string]map[string]bool) // helpers add := func(scopesByAction map[string]map[string]bool, action, scope string) { if _, ok := scopesByAction[action]; !ok { scopesByAction[action] = map[string]bool{scope: true} return } scopesByAction[action][scope] = true } includes := func(wildcardsSet map[string]bool, scope string) bool { for wildcard := range wildcardsSet { if wildcard == "*" || strings.HasPrefix(scope, wildcard[:len(wildcard)-1]) { return true } } return false } // Sort permissions (scopeless, wildcard, specific) for i := range ps { if ps[i].Scope == "" { if _, ok := reduced[ps[i].Action]; !ok { reduced[ps[i].Action] = nil } continue } if isWildcard(ps[i].Scope) { add(wildcardsByAction, ps[i].Action, ps[i].Scope) continue } add(scopesByAction, ps[i].Action, ps[i].Scope) } // Reduce wildcards for action, wildcards := range wildcardsByAction { for wildcard := range wildcards { if wildcard == "*" { reduced[action] = []string{wildcard} break } if includes(wildcards, wildcard[:len(wildcard)-2]) { continue } reduced[action] = append(reduced[action], wildcard) } } // Reduce specific for action, scopes := range scopesByAction { for scope := range scopes { if includes(wildcardsByAction[action], scope) { continue } reduced[action] = append(reduced[action], scope) } } return reduced } func ValidateScope(scope string) bool { prefix, last := scope[:len(scope)-1], scope[len(scope)-1] // verify that last char is either ':' or '/' if last character of scope is '*' if len(prefix) > 0 && last == '*' { lastChar := prefix[len(prefix)-1] if lastChar != ':' && lastChar != '/' { return false } } return !strings.ContainsAny(prefix, "*?") } func ManagedUserRoleName(userID int64) string { return fmt.Sprintf("managed:users:%d:permissions", userID) } func ManagedTeamRoleName(teamID int64) string { return fmt.Sprintf("managed:teams:%d:permissions", teamID) } func ManagedBuiltInRoleName(builtInRole string) string { return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(builtInRole)) } // GetOrgRoles returns legacy org roles for a user func GetOrgRoles(user identity.Requester) []string { roles := []string{string(user.GetOrgRole())} if user.GetIsGrafanaAdmin() { if user.GetOrgID() == GlobalOrgID { // A server admin is the admin of the global organization return []string{RoleGrafanaAdmin, string(org.RoleAdmin)} } roles = append(roles, RoleGrafanaAdmin) } return roles } func BackgroundUser(name string, orgID int64, role org.RoleType, permissions []Permission) identity.Requester { return &user.SignedInUser{ OrgID: orgID, OrgRole: role, Login: "grafana_" + name, Permissions: map[int64]map[string][]string{ orgID: GroupScopesByAction(permissions), }, } }