Skip to content

RBAC

Control who's allowed to do what. Declare each user's abilities, tag a handler with the permission it requires, and the framework checks it on every dispatch — before the handler runs, under every transport. @nwire/rbac does this on top of CASL.

Install

bash
pnpm add @nwire/rbac

Declare abilities

ts
import { defineAbility } from "@nwire/rbac"

export const abilities = defineAbility((user, { allow, deny }) => {
  allow("read", "Submission")

  if (user.role === "teacher") {
    allow("grade", "Submission", { classId: { $in: user.classIds } })
  }

  if (user.role === "student") {
    allow("submit", "Submission", { studentId: user.id })
  }

  deny("delete", "Submission")
})

Plug it in

ts
import { createApp } from "@nwire/app"
import { forgePlugins } from "@nwire/forge"
import { rbacPlugin } from "@nwire/rbac"

const app = createApp({
  appName: "submissions",
  handlers: [gradeSubmission],
  plugins: [...forgePlugins()],
})
  .with(rbacPlugin({ abilities }))

Tag the action

ts
const gradeSubmission = defineAction({
  name: "submissions.grade",
  policy: ["grade", "Submission"],
  input: z.object({ submissionId: z.string(), grade: z.number() }),
  emits: [SubmissionWasGraded],
  handler: async ({ input }) => SubmissionWasGraded(input),
})

When gradeSubmission is dispatched, the plugin checks user.can("grade", "Submission") against the loaded actor. If the check fails it throws ForbiddenError (mapped to 403 by the HTTP adopter, @nwire/koa).

Instance-level checks

For per-instance conditions ({ classId: { $in: ... } }), use subject(Submission, submission) to bind the actor data so CASL can match against the conditions.

ts
import { abilityFromCtx, subject } from "@nwire/rbac"

handler: async (ctx) => {
  const { input } = ctx
  const ability = abilityFromCtx(ctx)
  const submission = await ctx.actor(Submission, input.submissionId)

  if (ability.cannot("grade", subject("Submission", submission))) {
    throw new ForbiddenError()
  }

  return SubmissionWasGraded(input)
}

See also

MIT licensed.