Skip to main content
Arupa evaluates access in layers. The Kernel first resolves the identity of the requesting user, then applies the policies that protect the host path, the service, and the specific route the service registered. Every layer that applies to a request must allow it. A more specific policy does not override a broader restriction, and a service cannot use its own route policy to bypass Route.Allow or a service-wide Allow.

Access layers

The first three layers are configured by the Kernel configuration. The last layer is declared by the service when it registers a route. See Routes and transports for how a service registers routes and which part of the declaration carries the access policy.

User identity

For HTTP requests, the Kernel checks the arupa-auth session cookie first and then the Authorization header. A valid session resolves to a username. The Kernel looks up that username in Groups and creates a host-verified identity containing:
  • the username;
  • the groups that contain that username; and
  • the Authenticated state.
An invalid or missing session produces an unauthenticated identity. The Kernel does not send a user object to a service for an unauthenticated request.

Policy semantics

All access checks use the same policy model:
The result is determined as follows:
  • An empty policy is public. It allows unauthenticated users.
  • RequireAuth = true with no groups allows any authenticated user.
  • A non-empty Groups list requires authentication and membership in at least one listed group.
  • A user who is authenticated but belongs to none of the listed groups is forbidden.
The group list is an OR list. For example, Groups = ["root", "staff"] allows a user in root or staff; the user does not need to belong to both groups. The two rejection states are different:
  • An unauthenticated request is an authentication failure and produces 401 Unauthorized.
  • An authenticated user who does not satisfy the group requirement is an authorization failure and produces 403 Forbidden.

Denying access

Because an empty Groups list is public, there’s no way to write “deny everyone” by leaving Groups empty — an empty list and an absent policy behave the same way. To make a policy reject every request instead, name a group that has no members, for example Groups = ["nobody"]. Membership is determined entirely by the Groups table’s own membership lists, so a group name is never validated against anything when it’s used in a policy; nobody doesn’t need an entry in [Groups] at all. An undefined group simply has no members, which no user can ever belong to, so this works as a deny-all placeholder without requiring any bookkeeping elsewhere in the configuration. This applies to every layer on this page — Route.Allow, a service’s Allow, and a route’s own Access policy all resolve group membership the same way.

Host route access

Route.Allow protects HTTP paths before the request reaches either a host handler or a service handler:
The map value is a list of groups. A request is allowed when the authenticated user belongs to at least one group in the list. The Route.Allow map uses the Kernel’s shared path-pattern implementation. A normal path is an exact match, while a pattern ending in / matches that path subtree. The /* is not a valid configuration; it doesn’t support wildcard. A key can also be method-qualified by prefixing the path with an HTTP method and a colon, as in "GET:/api/list" above. A method-qualified rule only applies to requests using that method; a plain path with no METHOD: prefix applies to every method. A rule for GET also matches HEAD requests, since the two share caching semantics. The method must be an uppercase HTTP token — "get:/api/list" is invalid, and so is a colon with nothing in front of it, such as ":/api/list". Every key in Route.Allow is validated together: if any one of them is malformed, the whole update is rejected and none of the submitted changes are saved. When multiple Route.Allow patterns match a request, the longest matching path wins first. If more than one matching rule shares that same longest path — a method-qualified rule and a plain rule, or two rules for different methods — the more specific method wins: an exact method match beats a GET rule matching a HEAD request, which beats a plain, method-less rule. The Kernel does not combine groups from multiple matching rules; exactly one rule’s group list is evaluated. See Route for the complete path-pattern rules, root-path behavior, and validation details. If no rule matches, Route.Allow does not restrict the request. If a matching rule has an empty group list, its policy is also effectively public because an empty access policy allows everyone; an empty list does not mean “deny all”. See Denying access above for how to write a rule that actually rejects every request. Route.Allow is evaluated for every HTTP request by the outer auth middleware. When it rejects a request, it returns the normal JSON 401 or 403 response. It does not use the configured Pages redirect. See Pages for browser redirects used by service-protected resources.

Service-wide access

Allow under [Services.<name>] applies to the service as a whole:
This policy is checked before a request reaches any route owned by the service. It applies uniformly no matter which transport backs the route, including the service’s HTTP routes, whether they are handled dynamically or simply serve packaged files through a static transport. An empty Allow list leaves the service open at this layer. A non-empty list requires the user to belong to at least one of the listed groups. [Services.default] provides the base service configuration. A non-empty Allow list in [Services.<name>] replaces the default group list for that service. If the per-service list is omitted or empty, the current implementation keeps the default list. The service policy is refreshed when the Kernel reloads its configuration, so a loaded service uses the updated group list for subsequent requests and events.

Route access

A service can add another policy when it registers a route. This policy is not written in config.toml; it is part of the route declaration the service sends to the Kernel, and it applies the same way no matter which transport the route is bound to. An HTTP route carries its own Access policy. The Kernel checks the service-wide policy first and the route policy second:
This also covers static content: a route that serves packaged files through a static transport is still an HTTP route with its own Access policy, checked the same way as a route handled dynamically by the service. Because every check must pass, the effective permission is the intersection of the applicable layers. For example, a user must be in both staff from the service’s Allow and operators from a route’s Access.Groups to reach that route.

Example

With the following configuration:
The admin user can pass the /api/service/ host-route rule because they are in root. alice is authenticated but receives 403 Forbidden for that path because they are not in root. An unauthenticated request receives 401 Unauthorized. The same admin user cannot access any route protected by the hello service’s Allow = ["staff"] because admin is not in staff. Being authenticated is not enough when a policy names groups.