ddd-ts-kit/authz-in-application
Architecture id: authz/in-application. Default severity: error in recommended, error in strict.
Applies to: presentation/, infrastructure/ and domain/ of a context, tests excluded.
A permission decided in three places is decided in none. A role test in a route is skipped by the integration event handler that sends the same command; a refusal in a repository cannot be audited; an aggregate that knows roles cannot change authorization model. One place decides: the application layer, through the Authorizer port.
Reported
Section titled “Reported”In presentation/:
- a read of
.roles,.permissionsor.isAdminon anything - a comparison of a member named
rolewith a string (===,!==,==,!=), and aswitchon it - an import from
@ddd-ts-kit/auth(any entry) of anything but the request glue (actorOf,sessionOf,resolveSession,SessionAuthenticator,bearerCredential,inActorContext,principalResolver,HeaderValues), the codesFORBIDDENandUNAUTHORIZEDand the error classes (theAccessport included) - in a declared-style project (
stack.style: "declared"): an import ofRequires,PublicorPolicyfrom@ddd-ts-kit/kernel/declarations
In infrastructure/:
- a call to
.authorize(...),.decide(...)or.check(...)on something namedaccessor...authorizer..., or declared with the typeAccessor...Authorizer new ForbiddenError(...)- in a declared-style project: an import of
Requires,PublicorPolicyfrom@ddd-ts-kit/kernel/declarations, or of the portsAccessorPolicyAccessfrom@ddd-ts-kit/auth(any entry), as a type or as a value
In domain/:
- any import from
@ddd-ts-kit/auth - an import of
Actor,Actored,ActorContext,SessionorExecutionContextfrom@ddd-ts-kit/kernel - a read of
.rolesor.permissionson anything butthis
Incorrect:
if (!context.principal.roles.includes('controller')) { return { status: 403 };}
// infrastructure/persistence/drizzle-invoice.repository.tsawait this.access.authorize(actor, billingPermissions['invoice.read'], row);
// domain/aggregates/invoice.aggregate.tsvoid(by: Actor): void { if (!by.roles.includes('controller')) { throw new NotAllowedError(); }}Correct:
await commandBus.execute(new VoidInvoice(actorOf(context.principal), params.invoiceId));
// application/commands/void-invoice/void-invoice.handler.tsawait this.access.authorize(command.actor, billingPermissions['invoice.void'], invoice);invoice.void(command.actor.id, this.clock.now());Details
Section titled “Details”The type of a receiver is read from its declaration in the same file (a parameter, a variable, a property, a parameter property): no type information is needed. A class of infrastructure/ that implements an ...Authorizer is an adapter of the port (a cache, a remote service) and may delegate to another authorizer.
application/ is where access is declared and decided: Requires, Public and Policy of @ddd-ts-kit/kernel/declarations and the Access port of @ddd-ts-kit/auth are imported there freely, in both styles. In domain/ of a declared-style project the whole declarations entry is refused by layer-domain-purity, so this rule does not report it a second time. The other names of the declarations entry (Repository, CLOCK, ...) are not the concern of this rule. An explicit-style project is judged as before: these imports are not reported outside application/.
In domain/, this.roles and this.state.roles are not reported: a context whose subject is access itself (identity, membership) models roles as state.
Options
Section titled “Options”None.
rules."authz/in-application" in ddd-ts-kit.config.json set to off silences the rule, and // ddd-ts-kit-disable-next-line authz/in-application -- <reason> silences one line, exactly as for ddd-ts-kit check.
Limits
Section titled “Limits”Requires,PublicorPolicyreached through a namespace import of the declarations entry (import * as declarations) is not reported inpresentation/andinfrastructure/.- The names are the signal. A route of an administration API whose body legitimately carries
rolesorpermissions(grant a role to a member) is reported when it reads them: destructure the body (const { roles } = body) or record the deviation. - A role test written without a member named
roleorroles(isAdmin(user),['admin'].includes(kind)) is not seen. .check(...)on an authorizer held under another name and typed in another file is not seen.- An ownership test (
row.ownerId !== actor.id) is not recognised as such in any layer; ininfrastructure/only the refusal that follows (new ForbiddenError) is. Authenticatoris not on the allow-list ofpresentation/: the principal is resolved once by the server (principalResolver), not by each route.SessionAuthenticatoris, for the boundary that resolves a whole session withresolveSession.- A
Sessionor anExecutionContextdeclared by the project itself, or imported from another module than the kernel, is not reported indomain/.
With the CLI checker
Section titled “With the CLI checker”ddd-ts-kit check reports the same id from the text of the files and stays the fast edit-time guard. This rule is the authority.