Documentation
¶
Overview ¶
Package e2epolicy — audit.go
The policy's own rows in the audit log.
The routes that change the policy, a tenant's ceiling or a request are skipped by the audit middleware (auth.ActionForPath: /api/admin/e2e), and the rows are written here instead — the arrangement of internal/pluginreq — for two reasons. The middleware reads only the URL, so a policy change would be a bare `e2e.update` with no tenant and no before/after, and a request's approval would read as a "create". And one of the events has no route at all: an approval is spent at a create door (CheckCreate), whichever door that is. Written here, every event is one row that names what it acted on.
Package e2epolicy decides who may end-to-end encrypt what, and where (docs/E2E-ENCRYPTION.md → Who may encrypt, docs/PERMISSIONS.md → The permissions).
Encrypting is CREATING one of two names: a folder marker (.filex-e2e.json) where there was none — a new encrypted folder, or the first step of encrypting a folder in place — or a new single encrypted file (*.fxe). Everything else an encrypted folder does stays free: rewriting its marker (a new password, a recovery key, another level), adding files to it, taking the encryption off, copying or moving what is encrypted already (a folder, a `.fxe` that stays a `.fxe`; RelocationEncrypts).
Three layers must all say yes, asked in this order:
- the platform's ceiling for the tenant (providers.e2e_allowed, which only the supertenant changes): off binds administrators too;
- the tenant's policy (providers.e2e_policy; the e2e.policy setting on a single-tenant install): off, admins, permitted — the default, what filex always did — or approval;
- the person's files.encrypt on the path. Administrators always hold it: roles cannot narrow them, the policy can. Under the approval policy a person also needs an administrator's approval for that folder, good for one encryption and ApprovalTTL.
The server cannot see what a client encrypts, only the names it creates. So the rule is enforced where a name is created: every door that creates a file calls Service.CheckCreate at the moment it knows the write CREATES the path (nothing is catalogued there), and the explorer only mirrors the answer (Service.AnswerFor). The server decides.
Package e2epolicy — requests.go ¶
The approval half of the policy (`approval`): somebody who holds files.encrypt asks first, and an administrator of the tenant decides.
What a request is ¶
A PERSON, a FOLDER P and a KIND, for this account (operator decision 2026-10-03: the kinds are separate, and an approval opens only what was asked, in the folder it was asked for, never in a folder below it):
- folder: encrypt P itself, where it is (what P holds is encrypted in place);
- new_folder: make one NEW encrypted folder directly inside P, spent only on a folder that holds nothing (Service.approvalPlaces);
- file: one single encrypted file (`.fxe`) in P.
It is kept under ApprovalPath, where CheckCreate and AnswerFor look for it: a file's request is its folder's, because the name a `.fxe` is stored under cannot be known when the person asks. Approved, it opens exactly one encryption of its kind, once (CheckCreate marks it `used`; UseRecorder logs it, with the folder it was spent on), within ApprovalTTL of the approval. A request nobody decides expires after ApprovalTTL too, and past it nobody can decide it any more. One request per person, folder and kind waits at a time: asking again answers the one already waiting. A person has at most MaxPendingPerPerson waiting at once (too_many_pending past it).
A request is only ever what the rule itself answers `request` for (Service.AnswerForKind, asked with the request's kind): under the approval policy, with files.encrypt there, and no approval of that kind already waiting to be spent. What it names must be there and be of its kind: a folder for the two folder kinds, a file for a file (path_missing, kind_mismatch otherwise). Anything else would be a row an administrator could approve while the create door refused anyway, or never asked.
Who decides ¶
The administrators of the tenant whose policy asked for the approval (Service.TenantFor), which is what the row's provider_id records — nil on a single-tenant install. A tenant administrator lists and decides their own tenant's requests only, and another tenant's reads as missing (404, the reasoning of handlers/tenantown.go). A single-tenant install's administrators see and decide every request. The platform operator (an administrator of the supertenant on a multi-tenant install) SEES every tenant's requests but decides only the platform's own: the supertenant's, or one filed under no tenant. Another tenant's is that tenant's administrators' decision (not_decidable, MayDecide; operator decision 2026-10-03).
What is announced ¶
notify.EventE2ERequestCreated, to whoever decides it and to nobody else (announce): a tenant's request is ONE broadcast placed on the folder, so the bell hands it to the administrators of the tenant that storage belongs to, while the platform operator's bell does not take it (notify PlatformAdminBell); a webhook gets one delivery. The platform's own request on a multi-tenant install is addressed to each administrator of the supertenant instead, the webhook riding on the first of those rows. notify.EventE2ERequestDecided — to the requester alone.
Every step writes its own audit row (audit.go): create, approve, reject, expire, use.
Index ¶
- Constants
- Variables
- func ApprovalPath(rel, kind string) string
- func Audit(ctx context.Context, store db.Store, row AuditRow)
- func CarryInstancePolicy(ctx context.Context, store db.Store, multiTenant bool) (bool, error)
- func Decide(in Input) (Answer, Reason)
- func DoorError(u *model.User, st *model.Storage, err error) error
- func FileThere(ctx context.Context, drv storage.Driver, rel string) bool
- func IsEncryptionName(rel string) bool
- func MayDecide(r *model.E2ERequest, platform *int64) bool
- func RelocationEncrypts(ctx context.Context, srcDrv storage.Driver, src, dst string, sameStorage bool) bool
- func TTLDays() int
- func TargetOf(rel string) (dir string, kind string)
- func UseRecorder(store db.Store) func(ctx context.Context, r *model.E2ERequest, u *model.User, dir string)
- type Actor
- type Answer
- type Ask
- type AuditRow
- type CreateInput
- type Decision
- type Input
- type Options
- type Reason
- type RefusedError
- type RequestError
- type Requests
- func (q *Requests) Create(ctx context.Context, in CreateInput) (*model.E2ERequest, bool, error)
- func (q *Requests) Decide(ctx context.Context, d Decision) (*model.E2ERequest, error)
- func (q *Requests) ExpireDue(ctx context.Context) int
- func (q *Requests) List(ctx context.Context, tenant *int64, status string) ([]*model.E2ERequest, error)
- func (q *Requests) Mine(ctx context.Context, userID int64) ([]*model.E2ERequest, error)
- func (q *Requests) StartSweeper(ctx context.Context)
- type RequestsOptions
- type Service
- func (s *Service) AnswerFor(ctx context.Context, u *model.User, st *model.Storage, rel string) (Answer, Reason)
- func (s *Service) AnswerForKind(ctx context.Context, u *model.User, st *model.Storage, rel, kind string) (Answer, Reason)
- func (s *Service) AnswersFor(ctx context.Context, u *model.User, st *model.Storage, rels []string) []Answer
- func (s *Service) AnswersForKinds(ctx context.Context, u *model.User, st *model.Storage, asks []Ask) []Answer
- func (s *Service) CheckCopy(ctx context.Context, u *model.User, st *model.Storage, dst string, ...) error
- func (s *Service) CheckCreate(ctx context.Context, u *model.User, st *model.Storage, rel string) error
- func (s *Service) CheckCreateWithoutApproval(ctx context.Context, u *model.User, st *model.Storage, rel string) error
- func (s *Service) MultiTenant() bool
- func (s *Service) PolicyFor(ctx context.Context, providerID *int64) (model.ProviderE2E, error)
- func (s *Service) SourceIsFolder(ctx context.Context, srcStorageID int64, srcDrv storage.Driver, src string) (bool, error)
- func (s *Service) TenantFor(ctx context.Context, u *model.User, st *model.Storage) (*int64, error)
- func (s *Service) VerdictsFor(ctx context.Context, u *model.User, st *model.Storage, asks []Ask) []Verdict
- type Verdict
Constants ¶
const ( AuditActionPolicyUpdate = "e2e_policy.update" AuditActionTenantUpdate = "e2e_tenant.update" AuditActionRequestCreate = "e2e_request.create" AuditActionRequestApprove = "e2e_request.approve" AuditActionRequestReject = "e2e_request.reject" AuditActionRequestExpire = "e2e_request.expire" AuditActionRequestUse = "e2e_request.use" )
The actions. Constants, so the admin panel's audit labels are held to them (web/tests/lib/auditLabel.test.ts reads `AuditAction… = "…"`).
const ( AuditTargetPolicy = "e2e_policy" AuditTargetTenant = "providers" AuditTargetRequest = "e2e_request" )
The target types those rows name: a policy (the tenant's id, or none for the instance's), a tenant (its provider id), a request (its id).
const ApprovalTTL = 7 * 24 * time.Hour
ApprovalTTL is how long an approval may wait to be used, and how long a request may wait for a decision.
const MaxPendingPerPerson = 20
MaxPendingPerPerson is how many requests one person may have waiting at once (operator decision 2026-10-03). Every new request tells the administrators, so a script must not be able to bury them: the next one is refused (429 too_many_pending) until one of the waiting ones is decided or expires. Asking again for a request already waiting is not a new one and is always answered.
const SettingE2EPolicyCarried = "e2e.policy.carried"
SettingE2EPolicyCarried marks that a single-tenant install's policy (the e2e.policy setting) has been carried to the supertenant's row once, when the install first started multi-tenant (CarryInstancePolicy). Its value is when that happened. While it is there nothing is carried again.
Variables ¶
var ErrRefused = errors.New("e2e: encryption not allowed here")
ErrRefused is what every refusal of the rule unwraps to.
var ErrUndecided = errors.New("could not check the encryption policy")
ErrUndecided is what a door answers when the rule could not be decided: a lookup behind it failed (the tenant, the policy, the permissions, the approvals), or one the door made to ask it (the storage's row, the person a write is judged for). A server failure, never a refusal: a broken store must not read as the policy doing its job. Its words are the whole of it; what failed is in the log (DoorError).
Functions ¶
func ApprovalPath ¶
ApprovalPath is the folder an approval for encrypting rel as kind is kept under, and looked for by: rel itself for a folder (E2ERequestFolder: rel is the folder to encrypt; E2ERequestNewFolder: rel is the folder to make the new one in), the folder the file is in for a file. Requests are stored under it, and CheckCreate and AnswerFor find approvals through one function (Service.approvalPlaces), so the two cannot disagree.
⚠ A file's approval is its FOLDER's. The name a .fxe is stored under cannot be known when the person asks: a hidden name is random, a visible one takes the next free "name (2).ext.fxe". An approval is for this person, this folder, one encryption of this kind.
func Audit ¶
Audit writes one row. Best-effort, like every audit writer here: a row the store refuses is logged, never a reason to undo what already happened.
⚠ Names and flags only in Meta: the audit log is read by every administrator and exported (auth.AuditDetail says the same).
func CarryInstancePolicy ¶
CarryInstancePolicy keeps a single-tenant install's choice when it becomes a multi-tenant one (operator decision 2026-10-03).
On a single-tenant install the policy is the e2e.policy setting. Once FILEX_MULTI_TENANT is on, everybody with a tenant is judged by their tenant's row (providers.e2e_policy) instead, and the supertenant's row starts where migration 00080 put it: permitted. Read as it is, an install that had switched encryption off would find it silently on again for the platform's own people. So at the first multi-tenant start the setting is copied to the supertenant's row - only when that row still holds the migration's default and the setting says something else - logged, and written to the audit log. The setting itself is left alone (a return to single-tenant reads it again), and SettingE2EPolicyCarried is written, so a later start changes nothing, whatever an administrator has chosen since.
It answers whether it changed the supertenant's row. Nothing to carry - a single-tenant install, no setting, no supertenant, a setting nobody can read as a policy - is no change and no error.
func DoorError ¶
DoorError is what a door answers for err, the rule's verdict on u writing on st: nil and a refusal (*RefusedError) as they are, ErrUndecided for any other error. That error is logged here, once, with who and where (the storage and the person) and what failed, and never the path: a file's name can say as much as its contents. ErrUndecided itself passes unchanged, so a door may hand on what an earlier one decided.
func FileThere ¶
FileThere reports whether a FILE is at rel on drv: Stat answers, and not with a folder. Only then is writing a key file or a `.fxe` at rel a rewrite (a password change, a key slot), never a new encryption.
A folder with the name is not the file: an object store keeps the file beside it (a prefix and an object of one name), and a copy onto it replaces it. A Stat that fails for any reason, or no driver to ask, is not "it is there" either: a backend that could not answer must not switch the rule off. storage.Exists answers the other way round (for the snapshot guard) and is the wrong question here.
func IsEncryptionName ¶
IsEncryptionName reports whether creating rel encrypts something: its name is the folder marker or a single encrypted file's (".fxe"), either in any case. The name is all that counts; the server cannot read what a client meant.
func MayDecide ¶
func MayDecide(r *model.E2ERequest, platform *int64) bool
MayDecide reports whether an administrator may answer r. platform is the supertenant's id for the platform operator of a multi-tenant install, who answers only the platform's own requests (the supertenant's, or one filed under no tenant); nil for everybody else, whose reach is the list they are shown.
func RelocationEncrypts ¶
func RelocationEncrypts(ctx context.Context, srcDrv storage.Driver, src, dst string, sameStorage bool) bool
RelocationEncrypts reports whether giving the item at src the path dst (a rename or a move) is a new encryption, to be asked as a create at dst. A copy is Service.CheckCopy's question at the doors (operator decision 2026-10-03: a copy of what is encrypted makes a new encryption); the queue's worker reads this rule's names half for a queued copy too, after the handler asked CheckCopy (ops refuseUnsettledEncryption). Landing on a key file's (`.filex-e2e.json`) or a `.fxe`'s name is one, except in exactly three cases (operator decisions 2026-09-30 and 2026-10-03):
- src is a folder, under any name: a folder is no key file, and what it holds is not asked, as a folder copy's contents are not;
- a `.fxe` that stays a `.fxe`: it was encrypted already;
- a key file that stays the key file of its own folder (sameStorage, one folder: a change of case at most).
Everything else is asked: a plain file given either name, a `.fxe` given the key file's, a key file given a `.fxe`'s, and a key file moved into another folder, on this storage or another. Each makes an encryption nobody was asked about: upload a `.fxe` holding a key file's bytes, rename it `.filex-e2e.json`, and its folder is encrypted.
sameStorage: src and dst are on one storage. srcDrv is src's storage's driver. The names are read first, and src is looked at only when they do not decide. Only a clean "a folder" exempts it: a Stat that fails, or no driver to ask, counts as a file.
func TargetOf ¶
TargetOf is what creating rel encrypts, and where: the folder the marker sits in (the folder becoming encrypted), or the folder a .fxe goes into. kind is model.E2ERequestFolder or model.E2ERequestFile, and "" (with dir "") for a name that encrypts nothing. dir is storage-relative, with no leading slash; "" is the storage root.
⚠ The marker is matched in any case, as .fxe is. A case-insensitive storage keeps one file under ".filex-e2e.json" and ".FILEX-E2E.JSON", so a creation spelled the second way would escape the rule and still be read back as the marker.
func UseRecorder ¶
func UseRecorder(store db.Store) func(ctx context.Context, r *model.E2ERequest, u *model.User, dir string)
UseRecorder is Options.OnUse for a Service: the e2e_request.use row of an approval CheckCreate has just spent — the one event of a request no route sees, since it happens at whichever door creates the name. target_name is the approval's folder, meta `encrypted` the folder it was spent on (dir): for a folder approval of P, P itself or one folder directly inside it. For a create over a protocol this row is the one link between an encryption and its approval.
Types ¶
type Actor ¶
type Actor struct {
UserID *int64
Name string
// TokenID: the API key the call came through, when it came through one.
TokenID *int64
IP string
}
Actor is who did what a row records. The zero value is filex itself — a request that expired because nobody decided it.
type Answer ¶
type Answer string
Answer is the rule's verdict on one encryption at one place.
const ( // AnswerAllowed: go ahead. AnswerAllowed Answer = "allowed" // AnswerRequest: allowed once an administrator approves it. The explorer // offers "Request encryption" in its place. AnswerRequest Answer = "request" // AnswerDenied: not here, not by this person. AnswerDenied Answer = "denied" )
type Ask ¶
Ask is one question of AnswersForKinds: may the person encrypt Rel as Kind ("" for the kind the catalogue says).
type AuditRow ¶
type AuditRow struct {
Action string
TargetType string
TargetID string
// TargetName is what a reader wants once the thing has changed or gone:
// a tenant's name, a request's place ("Depo://a/b"). Stored as meta
// `target_name`, the key the Audit page reads (auth.SetAuditTarget).
TargetName string
Who Actor
Meta map[string]any
}
AuditRow is one row of the policy's log.
type CreateInput ¶
type CreateInput struct {
User *model.User
Storage *model.Storage
// Path is what the person asks about, relative to the storage: for kind
// folder the folder to encrypt where it is, for kind new_folder the folder
// to make a new encrypted folder in, and for kind file the file to
// encrypt.
Path string
Kind string
Reason string
Who Actor
}
CreateInput is one request as the requester asks for it.
type Decision ¶
type Decision struct {
ID int64
Approve bool
// Tenant is the deciding administrator's tenant; nil when nobody confines
// them. A request of another tenant reads as missing.
Tenant *int64
// Platform is the supertenant's id when the decider is the platform
// operator on a multi-tenant install (MayDecide): they see every tenant's
// request, and answer only the platform's own. nil: no such limit.
Platform *int64
Note string
Who Actor
}
Decision is an administrator's answer to one request.
type Input ¶
type Input struct {
// TenantAllowed is the platform's ceiling (always true on a single-tenant
// install).
TenantAllowed bool
// Policy is one of the model.E2EPolicy* values. Anything else is read as
// model.E2EPolicyPermitted, the default.
Policy string
// IsAdmin: an administrator of the install or of the tenant.
IsAdmin bool
// HasPermission: files.encrypt on the path (acl.Set.Can).
HasPermission bool
// HasApproval: an unused, unexpired approval for this person and folder.
HasApproval bool
}
Input is everything Decide weighs.
type Options ¶
type Options struct {
Store db.Store
// ACL answers files.encrypt on a path. Nil: acl.New(Store).
ACL *acl.Resolver
// MultiTenant reads the ceiling and the policy per tenant (providers).
// Off, the install's e2e.policy setting decides for everybody.
MultiTenant bool
// Now is the clock approvals expire by. Nil: time.Now.
Now func() time.Time
// Drivers reaches a storage's driver, for what the catalogue may not
// know yet: whether a folder is new (nothing in it) and whether a path a
// request names is there. Nil: the catalogue alone answers.
Drivers func(storageID int64) (storage.Driver, error)
// OnUse, when set, hears of every approval CheckCreate used up, after the
// row changed — the requests service writes its e2e_request.use audit row
// from it (UseRecorder). Never called for a lost race. dir is the folder
// the approval was spent on: for an in-place approval of P, P itself; for
// a new-folder approval of P, the new folder directly inside P; for a
// file approval, its folder P.
OnUse func(ctx context.Context, r *model.E2ERequest, u *model.User, dir string)
}
Options wires a Service.
type Reason ¶
type Reason string
Reason says which layer refused. It is the `reason` of a 403 e2e_not_allowed and the last segment of its message key (server.e2e.not_allowed.<reason>); "" with AnswerAllowed.
type RefusedError ¶
type RefusedError struct {
Reason Reason
}
RefusedError is the rule's refusal, naming the layer that refused. The HTTP doors answer it as 403 e2e_not_allowed with the reason.
func (*RefusedError) Error ¶
func (e *RefusedError) Error() string
func (*RefusedError) Unwrap ¶
func (e *RefusedError) Unwrap() error
Unwrap lets errors.Is(err, ErrRefused) find the refusal under any wrapping.
type RequestError ¶
type RequestError struct {
Status int
Code string
Message string
// Answer and Reason are the rule's own, when the refusal is that no
// request is needed here or none could help (code not_requestable).
Answer Answer
Reason Reason
}
RequestError is a refusal the HTTP layer answers as it is.
func (*RequestError) Error ¶
func (e *RequestError) Error() string
type Requests ¶
type Requests struct {
// contains filtered or unexported fields
}
Requests keeps the encryption requests.
func (*Requests) Create ¶
func (q *Requests) Create(ctx context.Context, in CreateInput) (*model.E2ERequest, bool, error)
Create files a request — or, when one already waits for this person, folder and kind, answers that one (created=false) and records nothing.
func (*Requests) Decide ¶
Decide approves or rejects a waiting request.
A request past its time is not waiting, whether or not a sweep has closed it yet: it expires here and is answered like one decided already. An approval would give it seven fresh days.
func (*Requests) ExpireDue ¶
ExpireDue closes what is past its time — a request nobody decided, an approval nobody spent — and answers how many. It runs before every read and write here, and hourly (StartSweeper).
The store hands it only what is due, the longest overdue first (E2ERequestFilter.ExpiresBefore), a batch at a time until a batch comes back short: one call closes every due row, however many newer rows there are. What it cannot read or write is logged and left to the next call. That delays the label, and opens nothing: Decide refuses a pending request past its time by itself, and FindApprovedE2ERequest an approval past its expiry, whatever their rows still say.
func (*Requests) List ¶
func (q *Requests) List(ctx context.Context, tenant *int64, status string) ([]*model.E2ERequest, error)
List answers the requests in one state, newest first: "" is pending, "all" every state. tenant keeps that tenant's only (nil: every tenant's).
func (*Requests) StartSweeper ¶
StartSweeper closes what is past its time now and every hour after, until ctx ends.
type RequestsOptions ¶
type RequestsOptions struct {
// Store keeps the rows (e2e_requests) and the audit log.
Store db.Store
// Policy answers whether a request is what the rule asks for, and whose
// tenant files it.
Policy *Service
// Notify tells the administrators and the requester (nil: nobody).
Notify notify.Service
Now func() time.Time
Log *slog.Logger
}
RequestsOptions wire a Requests.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service answers the rule against the store: the tenant's ceiling and policy, the person's permissions, and the approvals.
func (*Service) AnswerFor ¶
func (s *Service) AnswerFor(ctx context.Context, u *model.User, st *model.Storage, rel string) (Answer, Reason)
AnswerFor is the explorer's question: may u encrypt at rel on st, rel being the folder to encrypt in place, or the file to encrypt (AnswerForKind with the kind the catalogue says). It changes nothing: an approval is looked for, never used. When the answer cannot be found out - the tenant, the permissions or a lookup failed - it is denied (and logged): the menu hides what the server might refuse, and the server decides anyway.
func (*Service) AnswerForKind ¶
func (s *Service) AnswerForKind(ctx context.Context, u *model.User, st *model.Storage, rel, kind string) (Answer, Reason)
AnswerForKind is AnswerFor for one kind of encryption: E2ERequestFolder (rel is the folder to encrypt where it is), E2ERequestNewFolder (rel is the folder to make a new encrypted folder in), E2ERequestFile (rel is the file), or "" for the one the catalogue says. The approvals it looks for are the ones CheckCreate would spend for that encryption (approvalPlaces), so the explorer and the doors cannot disagree. A kind that is not what is at rel (a file asked about as a folder) is denied.
func (*Service) AnswersFor ¶
func (s *Service) AnswersFor(ctx context.Context, u *model.User, st *model.Storage, rels []string) []Answer
AnswersFor is AnswerFor for many paths on one storage, in their order.
func (*Service) AnswersForKinds ¶
func (s *Service) AnswersForKinds(ctx context.Context, u *model.User, st *model.Storage, asks []Ask) []Answer
AnswersForKinds is VerdictsFor's answers alone.
func (*Service) CheckCopy ¶
func (s *Service) CheckCopy(ctx context.Context, u *model.User, st *model.Storage, dst string, srcStorageID int64, src string, srcIsDir bool) error
CheckCopy is the rule at a door that COPIES the item at src (on srcStorageID; srcIsDir: it is a folder) to dst on st (operator decision 2026-10-03). A copy of an encrypted folder or of a `.fxe` makes a new encrypted item where it lands, exactly as creating one would, so it is asked as that create; a move and a rename are not (RelocationEncrypts).
- A file is asked by the name it lands under (CheckCreate at dst): a `.fxe` copied as a `.fxe`, a key file copied into another folder, a plain file copied onto either name. A `.fxe` copied under a plain name is no encryption by its name - the rule reads names (docs: the honest limits).
- A folder that holds a key file or a `.fxe` anywhere below it (the catalogue says, as the transfer guard reads it) is a NEW encrypted folder at dst: asked once, at dst, and under the approval policy it spends a new-folder approval of dst's folder. What it holds is looked up only when the rule would not let it through anyway, so an install whose policy lets this person encrypt pays nothing for it.
Under the approval policy it spends, as CheckCreate does: ask it once per copied item.
func (*Service) CheckCreate ¶
func (s *Service) CheckCreate(ctx context.Context, u *model.User, st *model.Storage, rel string) error
CheckCreate is the rule at a door. It answers nil when u may create rel on st, a *RefusedError when the rule refuses, and another error when it could not be decided — the door fails the write either way.
Call it where the door knows the write CREATES rel (nothing is catalogued there), and only there: rewriting a marker that exists is free (a new password, a recovery key), and so are moves of what is already encrypted (RelocationEncrypts). A copy is asked with CheckCopy. A name that encrypts nothing returns nil before anything is looked up. A caller with no person behind the write passes the account it acts for; nil is refused.
Under the approval policy the person's approval is used up here, before the bytes are written. ⚠ A write that fails afterwards does not give it back — the person asks again. Spending it after the write would let two parallel uploads use one approval twice.
func (*Service) CheckCreateWithoutApproval ¶
func (s *Service) CheckCreateWithoutApproval(ctx context.Context, u *model.User, st *model.Storage, rel string) error
CheckCreateWithoutApproval is CheckCreate that never uses an approval: where CheckCreate would spend one, it refuses with ReasonApprovalRequired, and nothing is recorded. Every other answer is CheckCreate's.
For a write judged as somebody who did not make it: a file request's upload is the link creator's file, but the visitor must not use up the approval the creator asked for (operator decision 2026-10-03).
func (*Service) MultiTenant ¶
MultiTenant reports whether the Service reads the ceiling and the policy per tenant (Options.MultiTenant).
func (*Service) PolicyFor ¶
PolicyFor is the ceiling and the policy that bind one tenant. A single-tenant install — and a caller with no tenant — has no ceiling and the install's setting for a policy. A policy nobody knows (a hand-written setting) reads as the default, permitted, which is what Decide does with it too.
func (*Service) SourceIsFolder ¶
func (s *Service) SourceIsFolder(ctx context.Context, srcStorageID int64, srcDrv storage.Driver, src string) (bool, error)
SourceIsFolder reports whether the item a copy reads at src (on srcStorageID) is a folder: srcDrv answers when the door has it, else the storage's driver (Options.Drivers), else the catalogue. Nothing there is not a folder (the copy fails on its own); a look that fails is an error, so a folder is never taken for a file on a guess.
func (*Service) TenantFor ¶
TenantFor is the tenant whose ceiling and policy bind u encrypting on st; nil on a single-tenant install, where the install's setting decides.
A tenant's own member is judged by their tenant: confinement keeps them on its storages anyway. A platform operator — a member of the supertenant — is judged by the tenant whose data it is: the storage's tenant (its lowest-id link, as tags' teamTenantFor picks it), else their own. A storage no tenant is linked to is the platform's.
func (*Service) VerdictsFor ¶
func (s *Service) VerdictsFor(ctx context.Context, u *model.User, st *model.Storage, asks []Ask) []Verdict
VerdictsFor is AnswerForKind for many questions on one storage, in their order, finding the tenant, the policy and the person's permissions once - the explorer asks for a whole listing at a time. The first lookup that fails ends the asking: it is logged once, and that question and every one after it is denied, instead of a failing store being asked again for each row.