perm

package
v0.49.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
const (
	PresetFullAdmin  = "full_admin"
	PresetStandard   = "standard"
	PresetReadOnly   = "read_only"
	PresetUploadOnly = "upload_only"
	PresetGuest      = "guest"
)

Preset names.

View Source
const AppPrefix = "app."

AppPrefix starts every app permission key.

View Source
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).

View Source
const MaxRuleNameLen = 100

MaxRuleNameLen bounds a rule's name.

View Source
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

View Source
var ErrInvalid = errors.New("perm: invalid")

ErrInvalid wraps every validation failure so callers can map it to a 400.

Guest can look and download, and cannot even change its own profile — a shared or demo login.

View Source
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.

View Source
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.

View Source
var UploadOnly = Of(FilesCreate, AccountEdit)

UploadOnly can put files in and nothing else — a drop-box account.

Functions

func AppKey

func AppKey(plugin, id string) string

AppKey builds the key for permission id of app plugin.

func Conditionable

func Conditionable(p Perm) bool

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 IsAppKey

func IsAppKey(k string) bool

IsAppKey reports whether k names an app permission ("app.sign.request").

func Known

func Known(k Perm) bool

Known reports whether k is in the catalogue.

func MatchPreset

func MatchPreset(s Set) string

MatchPreset returns the name of the preset equal to s, or "" (custom).

func Middleware

func Middleware(next http.Handler) http.Handler

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

func SaveAppDecisions(ctx context.Context, store db.Store, role string, m map[string]string) error

SaveAppDecisions replaces one built-in role's app decisions ({} clears them).

func SaveDefaults

func SaveDefaults(ctx context.Context, store db.Store, s Set) error

SaveDefaults stores the install's defaults for role=user. Role-only permissions are refused rather than silently dropped.

func SaveRoleBase

func SaveRoleBase(ctx context.Context, store db.Store, role string, s Set) error

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

func ValidAppDefault(d string) bool

ValidAppDefault reports whether d is one of the three.

func ValidateAppDecisions

func ValidateAppDecisions(m map[string]string) error

ValidateAppDecisions is SaveAppDecisions' check without the write.

func ValidateEffects

func ValidateEffects(m map[string]string) error

ValidateEffects checks an overrides or rule effects map: known keys, not role-only, allow/deny only.

func ValidateRoleBase

func ValidateRoleBase(role string, s Set) error

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.

func WithMemo

func WithMemo(ctx context.Context) context.Context

WithMemo returns ctx carrying a memo so a request that consults permissions several times (one acl.LoadSet per storage it touches) resolves each account once. Install it once per request, at the router.

Types

type AppDecisions

type AppDecisions map[string]map[string]string

AppDecisions is the built-in roles' app decisions: {"user": {"app.sign.request": "deny"}, "viewer": {...}}.

func LoadAppDecisions

func LoadAppDecisions(ctx context.Context, store db.Store) (AppDecisions, error)

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.

func All

func All() []Def

All returns the catalogue in UI order. The slice is a copy.

func Lookup

func Lookup(k Perm) (Def, bool)

Lookup returns the definition of k and whether it exists.

type Group

type Group string

Group is a UI grouping of the catalogue.

const (
	GroupFiles   Group = "files"
	GroupSharing Group = "sharing"
	GroupAccess  Group = "access"
	GroupAccount Group = "account"
	GroupAdmin   Group = "admin"
)

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

type Loader struct {
	Store db.Store
	// contains filtered or unexported fields
}

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 NewLoader

func NewLoader(store db.Store) *Loader

NewLoader returns a Loader backed by store.

func (*Loader) Load

func (l *Loader) Load(ctx context.Context, u *model.User) (*Result, error)

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

func (l *Loader) Preview(ctx context.Context, u *model.User, change func(*Input)) (*Result, error)

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 (
	ShareLinks       Perm = "share.links"
	ShareUploadLinks Perm = "share.upload_links"
	ShareUsers       Perm = "share.users"
	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"
	AdminShares  Perm = "admin.shares"
	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

func RelocateNeeds(srcStorage int64, src string, dstStorage int64, dst string) []Perm

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 Preset

type Preset struct {
	Name string
	Set  Set
}

Preset is a named, fixed Set offered as a one-click starting point.

func Presets

func Presets() []Preset

Presets returns the presets in UI order.

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

func Resolve(in Input) *Result

Resolve computes an account's permissions. Layers, lowest first:

  1. base — role=user: in.Defaults; role=viewer: in.ViewerBase (the Read-only preset when unset); other: nothing.
  2. 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).
  3. 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

func (r *Result) AllowedInFolders() Set

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) Can

func (r *Result) Can(p Perm) bool

Can reports whether p is allowed.

func (*Result) CanAt

func (r *Result) CanAt(storageID int64, rel string, p Perm) bool

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

func (r *Result) ConditionalRules() []int64

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

func (r *Result) VariesByFolder() Set

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.

func (*Result) Why

func (r *Result) Why(p Perm) Source

Why returns where p's answer came from.

func (*Result) WhyAt

func (r *Result) WhyAt(storageID int64, rel string, p Perm) Source

WhyAt is Why for one path.

type Set

type Set uint64

Set is a set of permissions as a bitset — the in-memory form only.

func FromStrings

func FromStrings(ss []string) Set

FromStrings parses persisted keys, dropping unknown ones (a permission removed from the catalogue must not fail a whole read).

func LoadDefaults

func LoadDefaults(ctx context.Context, store db.Store) (Set, error)

LoadDefaults returns the install's defaults for role=user: the stored set, or Standard when none was ever saved.

func LoadRoleBase

func LoadRoleBase(ctx context.Context, store db.Store, role string) (Set, error)

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 Of

func Of(ps ...Perm) Set

Of builds a Set; unknown keys are ignored.

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.

func (Set) Has

func (s Set) Has(p Perm) bool

Has reports whether p is in s. Unknown p is never held.

func (Set) Keys

func (s Set) Keys() []Perm

Keys returns the members of s in catalogue order.

func (Set) Len

func (s Set) Len() int

Len is the number of permissions in s.

func (Set) Strings

func (s Set) Strings() []string

Strings is Keys as plain strings — what gets persisted.

func (Set) With

func (s Set) With(p Perm) Set

With returns s plus p (unchanged for unknown p).

func (Set) Without

func (s Set) Without(p Perm) Set

Without returns s minus p.

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

func (s Source) RuleNameFor(lang string) string

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL