Authorization
- Owner mode (default)
- Web Access Control (WAC)
- What callers see when they may not
- Access Requests & Grants
Every access decision goes through a pluggable Authorizer, consulted by the service layer so that
the HTTP API, the management UI and the search services enforce exactly the same rules. Two models
are available via lws.access-control, and access grants layer on top of
whichever is chosen. Who the caller is comes from Authentication.
Owner mode (default)
lws.access-control=OWNER is single-tenant: the identities in lws.owners (WebIDs or DIDs) have
full control. Everyone else can read the storage only when lws.public-read=true, which is
storage-wide and defaults to false. To open a single resource or subtree to the world, issue an
access grant with assignee http://xmlns.com/foaf/0.1/Agent instead;
deleting it closes the access again on the next request.
The server refuses to start with no
lws.owners. An empty owner list is open mode — every read, write and control decision permitted for every client, anonymous included. It is still available for development, but has to be asked for withlws.dev.open=true. Open mode has no storage controller, so access grants cannot be issued in it at all.
Web Access Control (WAC)
Set lws.access-control=wac for multi-user authorization via
Web Access Control. Each resource may have an ACL resource
(<resource>.acl, or <container>/.acl), discoverable via the Link: rel="acl" header. That is the
address the ACL is read and written at; the graph itself is kept in the server-internal
urn:x-lws:acl: namespace, so no resource a client creates can ever be its own governing ACL. ACLs
are RDF documents containing acl:Authorization rules:
@prefix acl: <http://www.w3.org/ns/auth/acl#> .
# Bob may read & write everything under /shared/
<#bob> a acl:Authorization ;
acl:accessTo <http://localhost:8080/shared/> ;
acl:default <http://localhost:8080/shared/> ; # inherited by contained resources
acl:agent <https://bob.example/profile#me> ;
acl:mode acl:Read, acl:Write .
# Any signed-in agent may read it; the public may not
<#members> a acl:Authorization ;
acl:accessTo <http://localhost:8080/shared/> ;
acl:agentClass acl:AuthenticatedAgent ;
acl:mode acl:Read .
Rules
- Modes map to operations:
acl:Read→ GET/HEAD;acl:Append→ POST/PUT-create;acl:Write→ PUT-overwrite/PATCH/DELETE (Write implies Append);acl:Control→ read/write the resource’s ACL. - Agents:
acl:agent <webid>;acl:agentClass foaf:Agent(everyone, incl. anonymous) oracl:AuthenticatedAgent(any signed-in agent); oracl:agentGroup <group>— membership is resolved by dereferencing the group document (from the local store, or over HTTP for an external group, subject to the SSRF guard) and checkingvcard:hasMember. - Group resolution is cached for
lws.wac.group-cache-seconds(default 300). An external group document is fetched before the storage transaction that needs the decision opens, never inside it, so a slow host cannot stall writes. A group that cannot be resolved simply does not match (its authorization is skipped; the others still apply), is not remembered as empty, and is not retried forlws.wac.group-failure-cache-seconds(default 30). One decision dereferences at mostlws.wac.max-group-fetches-per-decision(default 8) documents. acl:originrestricts a rule to a browser/appOrigin(client-declared, not attested). A browser application needs both its origin inlws.cors.allowed-origins(so the browser lets it read responses) and, if the ACL restricts by origin, a matchingacl:origin.- Resolution: a resource’s own ACL (
acl:accessTo) wins; otherwise the nearest ancestor container’s ACL applies throughacl:default(inheritance), up to the root. The nearest ACL fully overrides ancestors — there is no super-owner, so include yourself when delegating a subtree. - Bootstrap: the root ACL is created at startup from
lws.owners(Read/Write/Control, plus public Read iflws.public-read). With no owners — which requireslws.dev.open=true— the root is opened to the public instead; that development ACL is marked as such and is rebuilt fromlws.ownersthe first time the server starts with owners configured. A root ACL you wrote yourself carries no marker and is never rebuilt (the server warns if it grants everyoneacl:Control). - Editing an ACL requires
acl:Controlon its target (ACLs are not themselves access-controlled, avoiding infinite regress).
To onboard a user, point them at their OIDC issuer — their WebID document must link that WebID (the
exact IRI the token’s sub carries) to an lws:OpenIdProvider service for that issuer, see
Authentication — and grant their WebID the modes they need in the relevant ACLs.
Self-sovereign users can instead authenticate with a controlled identifier (did:key, did:web or
an HTTPS CID document) and be named by that identifier.
What callers see when they may not
- An anonymous client that lacks access gets
401with theas_uri/realmchallenge, so it can discover how to authenticate. - An authenticated principal without Read gets
404for a resource that exists, the same as for one that does not, and theAllowheader onOPTIONSand405stops reporting its type —lws.mask-forbidden-as-not-found(defaulttrue). Set it tofalseto answer403instead. A principal who may read but not write still gets403for the write. - Container listings are filtered per member by read authorization:
itemsandtotalItemsreflect only the resources the client may read, so a listing is client-specific. The type index and type search apply the same per-request check.
Under WAC, the anonymous
401/404split can still reveal where ACLs sit; no status-code masking can hide that.
Access Requests & Grants
Access requests and grants — the LWS way for an agent to ask for access and for a controller to give
it without editing an ACL — have their own page. In short: a grant-aware
authorizer is layered over the base model, so an operation is permitted if the base model permits it
or an active grant authorizes it, and deleting the grant withdraws the access immediately. Grants
never confer Control.