ABAC Permissions
Efesto integrates CASL for Attribute-Based Access Control. The framework's whole contribution is small and precise:
- You declare a
permission: [action, model]tuple on an endpoint. - You build a CASL ability and put it on the request/context.
- Before running the handler, Efesto checks the ability against the tuple and rejects the request if it can't.
That's it. There are no permission decorators, no role hierarchies, no database loaders built into Efesto, those are patterns you build on top with CASL.
1. Build the ability (your code)
The ability is yours to create, usually in the auth middleware, from the
authenticated user. Both stacks expect it on a context field named ability by
default.
- Express
- Bun/Elysia
Attach the ability to req in a middleware that runs before Efesto:
import { defineAbility } from "@casl/ability";
app.use((req, res, next) => {
req.ability = defineAbility((can) => {
can("readAll", "User");
can("readOne", "User");
can("create", "User");
});
next();
});
app.use("/api/v1", efesto({ /* ... */ }));
Derive the ability in setup, so it is present in every route's context:
import { defineAbility } from "@casl/ability";
const app = efesto({
isProduction: false,
routesDir: `${import.meta.dir}/routes`,
prefix: "/api/v1",
setup: (root) =>
root.derive(() => ({
ability: defineAbility((can) => {
can("readAll", "Product");
can("create", "Product");
}),
})),
});
2. Declare the permission on the endpoint
The tuple is [action, model] on both stacks. Efesto checks it with
ability.can(action, model) before the handler runs.
- Express
- Bun/Elysia
Declare permission inside the method's _<method>Swagger object. It is written to
the generated Swagger and enforced at runtime:
import { BaseApiService, SwaggerOptions } from "efesto";
class Users extends BaseApiService {
constructor() {
super(__filename);
}
_getSwagger: SwaggerOptions = {
operationId: "getUsers",
permission: ["readAll", "User"],
responses: { 200: { content: { "application/json": { schema: { type: "array", items: "@User" } } } } },
};
_get(req, res) {
return res.json(listUsers());
}
}
export default Users;
Declare permission in the verb's _<verb>Swagger. Efesto compiles it into a
beforeHandle guard:
import { BaseApiService, type Context, RouteValidation, SwaggerOptions, t } from "efesto/elysia";
export default class extends BaseApiService {
_getSwagger: SwaggerOptions = { summary: "List products", permission: ["readAll", "Product"] };
_get() {
return listProducts();
}
_postSwagger: SwaggerOptions = { permission: ["create", "Product"] };
_postValidation: RouteValidation = { body: t.Object({ name: t.String() }) };
_post({ body }: Context) {
return createProduct(body);
}
}
3. What happens on failure
- Express
- Bun/Elysia
If the ability can't perform the action, Efesto raises a CASL
ForbiddenError
before the method runs. Handle it in your errorMiddleware to shape the response.
The compiled guard sets the status to 403 and returns { "error": "Forbidden" }
before the handler runs.
On both stacks the check is only enforced when an ability is present on the
context. If you never attach one, permission is documented but not enforced. The
Elysia guard mirrors the Express behavior here intentionally.
Configuration
Tune enforcement through configuration. Defaults match the descriptions above.
- Express
- Bun/Elysia
In the Efesto config object:
config: {
abacPermissions: {
actions: ["readAll", "readOne", "create", "update", "delete"], // documented actions
models: ["User", "Product"], // documented models
checkPermissionBeforeResolver: true, // enforce before the handler (default true)
reqAbilityField: "ability", // where Efesto reads the ability (default "ability")
},
}
| Property | Type | Description | Default |
|---|---|---|---|
actions | string[] | Available actions (typing/help for the tuple) | — |
models | string[] | Available models | — |
checkPermissionBeforeResolver | boolean | Enforce before the method runs | true |
reqAbilityField | string | req field holding the ability | "ability" |
Pass abac to efesto():
efesto({
// ...
abac: {
abilityField: "ability", // context field holding the ability (default "ability")
checkPermissionBeforeResolver: true, // enforce before the handler (default true)
},
});
Beyond the basics
Roles, ownership rules, time windows, and database-driven permissions are all
possible, but they are CASL features and your application logic, not Efesto
APIs. You express them when you defineAbility (step 1); Efesto only checks the
resulting ability against the declared tuple. See the
CASL documentation for conditions and field-level rules.
The ability is usually built in your authentication layer.