Documentation
¶
Overview ¶
Package perm is filex's per-user permission model: 28 named permissions, the presets built from them, and the resolver that turns an account's role, the install's defaults, the permission rules that match it and its own overrides into one answer per permission — with where that answer came from, so "why can't they do this?" has a reply.
It is the complement to package acl, not a replacement. acl answers "how far may this user reach into this path" (folder grants, the viewer ceiling, app-plugin locks); perm answers "which actions may this user take at all". A file action needs both (acl.Set.Can, Phase 2).
⚠ Permissions are persisted BY NAME (overrides, rules, the defaults setting), never by bit position. The bit layout of Set is a runtime detail and may be reordered freely; a stored name that is no longer in the catalogue is ignored on read.
Index ¶
- Constants
- Variables
- func AppKey(plugin, id string) string
- func Conditionable(p Perm) bool
- func HolderRole(r *model.PermissionRule, set Set) string
- func Invalidate()
- func IsAppKey(k string) bool
- func Known(k Perm) bool
- func MatchPreset(s Set) string
- func Middleware(next http.Handler) http.Handler
- func NormalizeRule(r *model.PermissionRule) error
- func NormalizeRuleEdit(r, prev *model.PermissionRule) error
- func RoleHolder(r *model.PermissionRule) string
- func SaveAppDecisions(ctx context.Context, store db.Store, role string, m map[string]string) error
- func SaveDefaults(ctx context.Context, store db.Store, s Set) error
- func SaveRoleBase(ctx context.Context, store db.Store, role string, s Set) error
- func StartingRole(rules []*model.PermissionRule, groups []string, providerID *int64) *model.PermissionRule
- func ValidAppDefault(d string) bool
- func ValidateAppDecisions(m map[string]string) error
- func ValidateEffects(m map[string]string) error
- func ValidateRoleBase(role string, s Set) error
- func WithMemo(ctx context.Context) context.Context
- type AppDecisions
- type AppDefault
- type Def
- type Group
- type Input
- type Loader
- type Perm
- type Preset
- type Result
- func (r *Result) AllowedInFolders() Set
- func (r *Result) AppAllowed(key string, def AppDefault) (bool, Source)
- func (r *Result) AppInherited(key string, def AppDefault) (bool, Source)
- func (r *Result) Can(p Perm) bool
- func (r *Result) CanAt(storageID int64, rel string, p Perm) bool
- func (r *Result) ConditionalRules() []int64
- func (r *Result) VariesByFolder() Set
- func (r *Result) Why(p Perm) Source
- func (r *Result) WhyAt(storageID int64, rel string, p Perm) Source
- type Set
- type Source
- type SourceKind
Constants ¶
const ( PresetFullAdmin = "full_admin" PresetStandard = "standard" PresetReadOnly = "read_only" PresetUploadOnly = "upload_only" PresetGuest = "guest" )
Preset names.
const AppPrefix = "app."
AppPrefix starts every app permission key.
const MaxRuleDescriptionLen = 1000
MaxRuleDescriptionLen bounds a translated description: what the role's own description column holds on MySQL (00069, VARCHAR(1000)). A translated name is bounded like the name (MaxRuleNameLen).
const MaxRuleNameLen = 100
MaxRuleNameLen bounds a rule's name.
const SnapshotTTL = 3 * time.Second
SnapshotTTL bounds how stale the cached defaults and rules may be. Writes in this process call Invalidate and are seen at once; the TTL is what carries a write made by ANOTHER filex process on the same database.
Variables ¶
var ErrInvalid = errors.New("perm: invalid")
ErrInvalid wraps every validation failure so callers can map it to a 400.
var Guest = Of(FilesDownload)
Guest can look and download, and cannot even change its own profile — a shared or demo login.
var ReadOnly = Standard & viewerCeiling
ReadOnly is what a role=viewer account could do before permissions existed: read, comment, personal tags, AI, apps, every protocol and token (read-only — acl caps the level), and its own profile. No file mutation, no sharing.
var Standard = filter(func(d Def) bool { return d.Group != GroupAdmin })
Standard is everything outside the admin area: exactly what a role=user account could do before permissions existed. It is the fallback for the install's defaults, so an upgrade changes nobody's access.
var UploadOnly = Of(FilesCreate, AccountEdit)
UploadOnly can put files in and nothing else — a drop-box account.
Functions ¶
func Conditionable ¶
Conditionable reports whether p may be changed by a rule with conditions.
func HolderRole ¶
func HolderRole(r *model.PermissionRule, set Set) string
HolderRole is the built-in role the people holding r get. It is not a choice: a role that can add, change, delete or share anything — everywhere or in some folders — needs the User level to do it; any other role is a Viewer's, which keeps a read-only role read-only on every door, even if an exception later allows a write.
func Invalidate ¶
func Invalidate()
Invalidate drops every Loader's cached defaults and rules. Call it after writing either.
func MatchPreset ¶
MatchPreset returns the name of the preset equal to s, or "" (custom).
func Middleware ¶
Middleware installs WithMemo on every request.
func NormalizeRule ¶
func NormalizeRule(r *model.PermissionRule) error
NormalizeRule validates r in place and canonicalizes what it can (trimmed name and targets, lowercase dot-less extensions). It does not check that user ids or the provider exist — that is the handler's job, with the store.
func NormalizeRuleEdit ¶
func NormalizeRuleEdit(r, prev *model.PermissionRule) error
NormalizeRuleEdit is NormalizeRule for a new version of prev (nil for a new role): the only difference is that a language prev's translations already carry stays acceptable (normalizeRuleTexts).
func RoleHolder ¶
func RoleHolder(r *model.PermissionRule) string
RoleHolder is HolderRole for a role as stored.
func SaveAppDecisions ¶
SaveAppDecisions replaces one built-in role's app decisions ({} clears them).
func SaveDefaults ¶
SaveDefaults stores the install's defaults for role=user. Role-only permissions are refused rather than silently dropped.
func SaveRoleBase ¶
SaveRoleBase stores the base of a built-in role. A viewer's is refused if it names a permission a viewer can never hold, rather than silently trimmed.
func StartingRole ¶
func StartingRole(rules []*model.PermissionRule, groups []string, providerID *int64) *model.PermissionRule
StartingRole is the custom role a NEW account is given when its first SSO sign-in carries one of the groups a role names: the lowest-numbered enabled role in the account's tenant with a matching sso_group target, or nil.
func ValidAppDefault ¶
ValidAppDefault reports whether d is one of the three.
func ValidateAppDecisions ¶
ValidateAppDecisions is SaveAppDecisions' check without the write.
func ValidateEffects ¶
ValidateEffects checks an overrides or rule effects map: known keys, not role-only, allow/deny only.
func ValidateRoleBase ¶
ValidateRoleBase is SaveRoleBase's check without the write: role=user may not hold a role-only permission, role=viewer nothing past the viewer ceiling, and no other role has an editable base.
Types ¶
type AppDecisions ¶
AppDecisions is the built-in roles' app decisions: {"user": {"app.sign.request": "deny"}, "viewer": {...}}.
func LoadAppDecisions ¶
LoadAppDecisions reads the built-in roles' app decisions; none is empty.
type AppDefault ¶
type AppDefault string
AppDefault is who holds an app permission nobody has decided: the app's manifest picks it, the administrator can change it per role and per person.
const ( // AppDefaultViewer: every account (viewers included). AppDefaultViewer AppDefault = "viewer" // AppDefaultUser: accounts that can change files — not viewers. AppDefaultUser AppDefault = "user" // AppDefaultAdmin: administrators only, until granted. AppDefaultAdmin AppDefault = "admin" )
type Def ¶
type Def struct {
Key Perm
Group Group
// RoleOnly entries are held only through the account role; rules and
// overrides naming them are ignored (and refused by Validate*).
RoleOnly bool
// ViewerCapped entries can never be allowed for a role=viewer account,
// whatever a rule or override says — the same line acl.RoleCeiling draws
// at LevelViewer for file mutation, extended to sharing (which needs
// editor level today, see handlers/share.go).
ViewerCapped bool
}
Def describes one catalogue entry.
type Input ¶
type Input struct {
UserID int64
Role string
ProviderID *int64
// CustomRoleID is the one custom role the account holds
// (user_custom_roles); 0 is none.
CustomRoleID int64
// Defaults is the base set for role=user (model.SettingPermissionDefaults).
Defaults Set
// ViewerBase is the base set for role=viewer
// (model.SettingPermissionViewerDefaults); nil is the Read-only preset.
ViewerBase *Set
// Rules is every custom role on the install; Resolve applies the
// account's own (CustomRoleID), if it is enabled and in scope.
Rules []*model.PermissionRule
// Overrides is the user's own allow/deny map (keys are Perm names, and
// app permission keys — perm/app.go).
Overrides map[string]string
// AppDecisions are the account's built-in role's decisions about app
// permissions (SettingPermissionAppDefaults for its role).
AppDecisions map[string]string
}
Input is everything Resolve needs about one account. The Loader fills it from the store; tests build it by hand.
type Loader ¶
Loader reads what Resolve needs from the store.
⚠ Any read failure is returned, never papered over with a default: the caller denies on error (the same rule acl.Resolver follows). Falling back to the Standard preset when the defaults row is unreadable would hand an install that had narrowed its defaults the full set back.
func (*Loader) Load ¶
Load resolves u's permissions. A nil user holds nothing. An administrator is answered without touching the store. Within a request carrying a memo (WithMemo), an account is resolved once.
func (*Loader) Preview ¶
Preview resolves u as Load would after change has been applied to the input: what the account WOULD hold with other exceptions, another custom role or another built-in role. u may be a new account that is not stored yet (id 0: no exceptions, no custom role). The per-request memo is neither read nor filled — the answer is about a state that does not exist.
It is how a delegated administrator's change is judged by its RESULT rather than by the words of the request: lifting a Deny, ending a restrictive custom role or picking a built-in role all hand out permissions without the request naming a single one.
type Perm ¶
type Perm string
Perm is one permission, identified by its stable key.
const ( FilesDownload Perm = "files.download" FilesCreate Perm = "files.create" FilesModify Perm = "files.modify" FilesRename Perm = "files.rename" // a new name in the same folder FilesMove Perm = "files.move" // to another folder FilesDelete Perm = "files.delete" // to trash FilesPurge Perm = "files.purge" // permanent delete, empty trash FilesTag Perm = "files.tag" )
File actions. Path-scopable; each maps to an operation every protocol has.
const ( CommentsWrite Perm = "comments.write" AIUse Perm = "ai.use" PluginsRun Perm = "plugins.run" )
Sharing and features.
const ( AccessWebDAV Perm = "access.webdav" AccessSFTP Perm = "access.sftp" AccessFTP Perm = "access.ftp" AccessS3 Perm = "access.s3" AccessNFS Perm = "access.nfs" AccessAPI Perm = "access.api" AccessDesktop Perm = "access.desktop" )
Access outside the web app.
const ( AdminUsers Perm = "admin.users" AdminGrants Perm = "admin.grants" AdminAudit Perm = "admin.audit" AdminMonitor Perm = "admin.monitor" // AdminFull is the account role `admin`, surfaced as a permission so the // UI can show it. It is never granted by a rule or an override: storages, // settings, SSO, plugins and updates each amount to full control of the // server, so the only way to hold them is to BE an administrator. AdminFull Perm = "admin.full" )
Admin area.
const (
AccountEdit Perm = "account.edit"
)
Own account.
func RelocateNeeds ¶
RelocateNeeds is what taking a path from src to dst is: a new name in the same folder is files.rename, the same name in another folder (or on another storage) files.move, and both at once needs both. Paths are storage-relative. A path that does not change still counts as a rename, so every caller has something to check.
type Result ¶
type Result struct {
Allowed Set
Sources map[Perm]Source
// Settings is the most restrictive merge of every applying rule's
// settings.
Settings model.PermRuleSettings
// Rules are the ids of the rules that applied, in input order.
Rules []int64
// contains filtered or unexported fields
}
Result is one account's resolved permissions.
func Resolve ¶
Resolve computes an account's permissions. Layers, lowest first:
- base — role=user: in.Defaults; role=viewer: in.ViewerBase (the Read-only preset when unset); other: nothing.
- the account's custom role (one per person, CustomRoleID), when it is enabled and in the account's tenant: its own list REPLACES the base, and its "different in some folders" part applies per path (CanAt).
- overrides — the user's own allow/deny; always wins over rules.
Then two hard lines no layer can cross: role-only permissions (admin.full) follow the role alone, and a viewer account never holds a ViewerCapped permission. role=admin short-circuits everything: an administrator holds every permission and is bound by no rule (they could edit the rule anyway).
func (*Result) AllowedInFolders ¶
AllowedInFolders is what the account may do only in some folders: the permissions its role's "different in some folders" part allows that it does not hold everywhere — minus what its own exceptions deny, and what a viewer can never hold. A client offers these actions and lets the server decide per path (CanAt); it must not hide them.
func (*Result) AppAllowed ¶
func (r *Result) AppAllowed(key string, def AppDefault) (bool, Source)
AppAllowed decides the app permission key for this account. def is the app's manifest default for it. Layers, highest first: the administrator role, the person's own exception, their custom role's decision, the built-in role's decision, the app default.
func (*Result) AppInherited ¶
func (r *Result) AppInherited(key string, def AppDefault) (bool, Source)
AppInherited is AppAllowed without the person's own exception: what the account would get from its role and the app's default alone — the answer a person's "Default" choice stands for on their exceptions editor.
func (*Result) CanAt ¶
CanAt is Can for an action on one path of one storage: the same answer, except where a conditioned rule (model.PermRuleConditions) matches — its allow or deny then counts as a rule's does, beneath the user's overrides.
func (*Result) ConditionalRules ¶
ConditionalRules returns the ids of the applying rules that bind only some storages or paths — they are not in Rules, and not in Allowed.
func (*Result) VariesByFolder ¶
VariesByFolder is every permission whose answer can differ from path to path: those the role's folder part allows or denies somewhere — minus what an exception of the account's own fixes everywhere, and what a viewer can never hold. A client asks the server (CanAt, per path) about these before offering them, and trusts Allowed for the rest.
type Set ¶
type Set uint64
Set is a set of permissions as a bitset — the in-memory form only.
func FromStrings ¶
FromStrings parses persisted keys, dropping unknown ones (a permission removed from the catalogue must not fail a whole read).
func LoadDefaults ¶
LoadDefaults returns the install's defaults for role=user: the stored set, or Standard when none was ever saved.
func LoadRoleBase ¶
LoadRoleBase returns the editable base of a built-in role: the defaults for role=user, the viewer defaults (capped at the viewer ceiling) for role=viewer. Administrators have no editable base — everything is theirs.
func RoleSet ¶
func RoleSet(r *model.PermissionRule) Set
RoleSet is what a custom role allows everywhere: its own list.
func ViewerCeiling ¶
func ViewerCeiling() Set
ViewerCeiling is every permission a viewer account can ever hold.
type Source ¶
type Source struct {
Kind SourceKind `json:"kind"`
RuleID int64 `json:"rule_id,omitempty"`
RuleName string `json:"rule_name,omitempty"`
// RuleNames are the role's names in other languages (migration 00071),
// kept so a refusal can name the role in its reader's language
// (RuleNameFor). Not on the wire: rule_name stays the role's own name,
// and the admin pages hold the translations with the role itself.
RuleNames map[string]string `json:"-"`
}
Source is where one permission's answer came from.
func (Source) RuleNameFor ¶
RuleNameFor is the deciding role's name for a reader of lang: its translation, else its own name (model.LocalizedText, the one rule PermissionRule.NameFor follows too).
type SourceKind ¶
type SourceKind string
SourceKind says which layer decided a permission.
const ( // SourceRole: the account role decided outright (admin holds everything; // admin.full follows the role alone). SourceRole SourceKind = "role" // SourceBase: nothing overrode it; it is the role's starting set (the // install defaults for role=user, the Read-only preset for role=viewer). SourceBase SourceKind = "base" // SourceRule: the account's custom role decided it (its list, or its // "different in some folders" part). SourceRule SourceKind = "rule" // SourceOverride: the user's own override. SourceOverride SourceKind = "override" // SourceViewerCeiling: something allowed it, but a viewer account can // never hold it. SourceViewerCeiling SourceKind = "viewer_ceiling" // SourceRoleOff: the account's custom role is switched off, and a // switched-off role gives nothing (never the built-in role instead — for // a role that takes things away, that would be MORE access). SourceRoleOff SourceKind = "role_off" )
const SourceAppDefault SourceKind = "app_default"
SourceAppDefault is an app permission decided by the app's own default.