Documentation
¶
Overview ¶
Package apitoken implements bearer-token authentication for non-interactive callers: AI agents, the work.example.com FilexClient, and the embedded MCP server.
A token is a 64-char hex string handed out once at create time (POST /api/admin/ai-tokens). Only its sha256 hash is stored in the api_tokens table. Every token is bound to a user, and a request authenticated by this driver carries BOTH the user and the token through auth.Middleware (see AuthenticateToken), so the gates downstream — RequireAdmin among them — judge it by what the token grants, not by the account's role.
Credentials are read from either header:
X-Filex-Token: <token> Authorization: Bearer <token>
The matched *model.APIToken is attached to the request context so downstream handlers can enforce per-token scopes (see auth.TokenFrom).
Index ¶
- Constants
- Variables
- func ExtractToken(r *http.Request) string
- func HashToken(raw string) string
- func IsValidScope(s string) bool
- func JoinScopes(verbs, roots []string) string
- func ParseIssued(raw string) (verbs, roots []string, err error)
- type Driver
- func (d *Driver) Authenticate(r *http.Request) (*model.User, error)
- func (d *Driver) AuthenticateToken(r *http.Request) (*model.User, *model.APIToken, error)
- func (d *Driver) Capabilities() auth.Capabilities
- func (d *Driver) Init(_ context.Context, _ map[string]any) error
- func (d *Driver) Name() string
- type UnknownScopeError
Constants ¶
const ( ScopeRead = "read" ScopeWrite = "write" ScopeDelete = "delete" ScopeMCP = "mcp" ScopeAdmin = "admin" )
Issuable token scopes. A token grants exactly the scopes in its list — an empty list grants NOTHING (see ParseIssued in issue.go for why, and for the one rule every door that mints a token applies). RequireScope gates each verb against these; the issuers reject anything not in the set so an operator can't mint a token carrying a typo'd scope that silently grants nothing.
read — list / info / download / search (REST) — read-only file ops
write — upload / mkdir / move (REST) — file mutations
delete — delete (REST) — soft-delete files
mcp — the streamable-HTTP MCP server at /api/ai/mcp
admin — the full admin surface at /api/ai/admin/* and the admin_*
MCP tools (users, storages, settings, replica, queue, …)
const HeaderName = "X-Filex-Token"
HeaderName is the dedicated header AI/MCP callers send.
const ScopeRootPrefix = "root:"
ScopeRootPrefix marks a path-confinement scope: `root:<adapter>://<rel>`. Unlike the verb scopes it carries a value and is enforced by package confine as a hard path ceiling — across both /api/files and the AI surface. A token may combine verb scopes with at most one root scope.
Variables ¶
var ErrScopesRequired = errors.New("at least one scope is required (read, write, delete, mcp or admin)")
ErrScopesRequired is an issuance request that names no verb scope.
var ValidScopes = []string{ScopeRead, ScopeWrite, ScopeDelete, ScopeMCP, ScopeAdmin}
ValidScopes is the canonical, ordered set of issuable VERB scopes. The `root:` confinement scope is validated separately (see IsValidScope).
Functions ¶
func ExtractToken ¶
ExtractToken pulls the raw token from the X-Filex-Token header first, then falls back to `Authorization: Bearer <token>`.
func IsValidScope ¶
IsValidScope reports whether s is a known issuable scope — a verb scope or a well-formed `root:<adapter>://<rel>` confinement scope.
⚠ "Well-formed" is decided by the confinement parser itself, not by a non-empty check. A root scope it cannot read (`root:projects`, no storage named) used to be accepted here and then ignored by every surface that enforces confinement, so the operator believed they had issued a folder credential and had in fact issued an unconfined one.
func JoinScopes ¶
JoinScopes is the stored form of a parsed list: the verbs, then the roots.
func ParseIssued ¶
ParseIssued validates a requested scope list for a NEW token: every entry is a known verb or a well-formed `root:` confinement, duplicates collapse, and at least one VERB is present — a root on its own grants no verb, so it is refused like an empty list rather than minted as a token that can do nothing. It returns the verbs (in the order ValidScopes lists them) and the roots (in the order given).
Types ¶
type Driver ¶
type Driver struct {
// contains filtered or unexported fields
}
Driver is the API-token auth driver.
func (*Driver) Authenticate ¶
Authenticate resolves a bearer/X-Filex-Token credential to its bound user. Returns auth.ErrUnauthorized (so the middleware falls through to the next driver) when no credential is present or it doesn't validate.
⚠ The user alone is not the whole answer for a token: see AuthenticateToken, which is what auth.Middleware actually calls.
func (*Driver) AuthenticateToken ¶
AuthenticateToken implements auth.TokenAuthenticator: it returns the bound user AND the matched token, so the chain attaches the token to the request context and every scope and confinement check downstream can see it.
⚠ Returning only the user was the defect: a request authenticated here then looked, to every gate after it, like the account's own session, and was judged by the account's role instead of by the token's scopes.
func (*Driver) Capabilities ¶
func (d *Driver) Capabilities() auth.Capabilities
Capabilities — token auth is headless; no interactive sign-in surface.
type UnknownScopeError ¶
type UnknownScopeError struct{ Scope string }
UnknownScopeError names a scope this server does not issue.
func (*UnknownScopeError) Error ¶
func (e *UnknownScopeError) Error() string