first commit
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# middleware/require-role
|
||||
|
||||
## Purpose
|
||||
|
||||
`requireRole(...roles)` builds a Hono middleware that gates a route on
|
||||
the principal carrying at least one of the supplied roles. The factory
|
||||
throws an `AuthError(AUTH_INVALID_INPUT)` at construction time when
|
||||
called with no roles — a guard against accidentally building a
|
||||
no-op middleware.
|
||||
|
||||
## Features
|
||||
|
||||
- Constant-time membership check against an internal `Set<string>`.
|
||||
- Two failure modes — `AUTH_UNAUTHENTICATED` (401) for anonymous
|
||||
callers, `AUTH_INSUFFICIENT_ROLE` (403) for callers whose
|
||||
`principal.roles` does not intersect the required set.
|
||||
- Emits `auth.role_check_total{outcome}` for every call, with
|
||||
`outcome ∈ "allowed" | "denied"`.
|
||||
- Carries `context.required` and `context.actual` on the 403 error so
|
||||
audit logs / OpenAPI responses can show the precise mismatch.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { requireAuth, requireRole } from "@suaveplan/api-plugin-auth/middleware";
|
||||
|
||||
app.use("/admin/*", requireAuth(), requireRole("admin"));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose multiple roles for resource-level scoping. Combine with
|
||||
`AuthService` lookups to project derived roles (e.g. organisation
|
||||
membership) onto the request before the middleware runs:
|
||||
|
||||
```ts
|
||||
import { requireAuth, requireRole } from "@suaveplan/api-plugin-auth/middleware";
|
||||
|
||||
app.use("/projects/:id/edit", requireAuth(), async (c, next) => {
|
||||
const principal = c.get("user");
|
||||
if (!principal) return next();
|
||||
const isOwner = await checkOwnership(c.req.param("id"), principal.id);
|
||||
if (isOwner) {
|
||||
c.set("user", { ...principal, roles: [...principal.roles, "owner"] });
|
||||
}
|
||||
await next();
|
||||
}, requireRole("admin", "owner"));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `function requireRole(...roles: readonly string[]): MiddlewareHandler`
|
||||
|
||||
- Throws `AuthError(AUTH_INVALID_INPUT)` at construction when `roles`
|
||||
is empty.
|
||||
- Throws `AuthError(AUTH_UNAUTHENTICATED)` (401) when `c.get("user")`
|
||||
is null/undefined.
|
||||
- Throws `AuthError(AUTH_INSUFFICIENT_ROLE)` (403) when no role in
|
||||
`principal.roles` is in the required set.
|
||||
- Emits `auth.role_check_total{outcome="allowed"|"denied"}` on every
|
||||
request.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The required-roles set is constructed once per factory call. Reusing
|
||||
the same middleware across multiple routes is constant memory.
|
||||
- The error context carries the actual roles as well as the required
|
||||
ones so the 403 response can surface a "missing role: admin"
|
||||
message in operator logs without re-reading the principal.
|
||||
- The middleware never inspects `principal.tenantId` — tenant-scoped
|
||||
role gates are the responsibility of `@suaveplan/api-tenant`'s
|
||||
middleware, layered downstream.
|
||||
Reference in New Issue
Block a user