Permissions on actions
Most actions need no permission of their own: an action takes its
permission from the entities it touches. Root policy.jadpo lists the few
that can't, under actions::
- an action that touches no entity, such as an operator's health report, which is otherwise refused;
- an action that needs different people from its entity's grants, such as handing a todo to someone else, or creating a company together with its first owner's membership.
Each line names an action by its domain and name, and lists who may run it, with the same kinds of entry as a grant:
// policy.jadpopolicy { tenant: Company actions: { todos.transfer_todo: [Todo.owner_id] accounts.create_company: [Access.authenticated] monitoring.health_report: [StaffRole.operator] }}
The domain is part of the name, as in a call from another domain. A bare
name that is one domain's action, transfer_todo:, is
policy.binding_invalid with the edit that writes todos.transfer_todo:,
and the use line that brought the bare name in isn't also reported
unused; jadpo fmt then writes use todos in its place.
Todo.owner_id means the current owner of the todo the action changes.
For that action, the listed people replace the entity grants on the
creates, updates, deletes and transitions in its own body. Its reads, and
the actions it calls, keep their own entities' grants, with the same
caller. Field restrictions and
tenancy still apply.
Calling an integration, an outside service such as a payment provider, needs nothing extra: permission to run the action covers it.
Lines that depend on a record
A user field such as Todo.owner_id, or a role held in a record such as
CompanyRole.owner, is checked on the records the action itself creates,
changes or deletes. On an action that changes none of them, such as a
report, it lets nobody in. The action is then checked as it starts against
the line's other entries: anyone signed in, or a role held across the
whole application. With none left, it refuses every caller, so its route
is policy.effect_ungranted.
A call the listing doesn't cover
Because a called action keeps its own grants, a listed action can call
one that refuses some of the people its line lets in. Say
lending.borrow_book is listed for Access.authenticated and calls
lend_book, whose Book update only StaffRole.admin holds. A customer
would get 403 from lend_book. jadpo check reports this at the call as
policy.call_ungranted, naming the people the line lets in that the
called action refuses. Fix it by making the write in the listed action's
own body, or by listing the called action for exactly those people.
Listing it for everyone widens it on every path that reaches it. It is
an error you can decline, for a call that only some callers are meant to
pass.
Listing a query
A query is listed under actions: like an action, when it needs fewer
people than its entity's read:. Each listed query's line may only hold
entries that depend on no record: Access.authenticated, or a role held
across the whole application. Anything else is policy.binding_invalid.
The line is checked as the query starts, on every request, so removing
someone's role takes effect on their next request. Its records are still
filtered by the entity's read:, and its output fields are proved
against the line's people, not everyone the entity's read: lets in. So
one entity can give staff a query with stock and sale fields restricted
to StaffRole.admin, beside a catalogue query for every signed-in
customer.
A route anyone may call can't run a listed query, since its line never
names Access.public (policy.public_grant_missing). A schedule,
subscriber or batch never passes one either: the system is let in only
by a line naming Access.system, which a query's can't. Reaching one is
policy.system_grant_missing.
Here every signed-in customer reads a book, and only a bookseller where it is shelved:
use shop/types { StaffRole }use shoppolicy { actions: { shop.stock: [StaffRole.bookseller] }}
use shop/types { StaffRole, Title }/// A book every signed-in customer may read, with where it is shelved,/// which only a bookseller may.entity Book { id: Uuid title: Title shelf_note: Title { policy: { read: [StaffRole.bookseller] } } policy: { create: [StaffRole.bookseller] read: [Access.authenticated] }}
Unlisted, stock would be policy.output_field_unproved, since
anyone signed in may call it and not all of them may read shelf_note.
Listed, it is proved against StaffRole.bookseller alone:
use shop/entities { Book }use shop/types { StockBook }/// Every book with where it is shelved, for staff.query stock() -> List<StockBook> { from: Book order: [Book.title asc] limit: 50}
A customer is refused the stock with 403 not_permitted, and a member
of staff whose role is removed is refused on their next request:
use shop/entities { Book, Member, Staff }use shop/types { CatalogueBook, StockBook }use shop/actions { add_book, appoint, register, stand_down }test "staff see where a book is shelved, and customers only the catalogue" { var ann: Member = attempt register("ann") var cyd: Member = attempt register("cyd") var staff: Staff = attempt appoint(ann.id) assert staff.member_id == ann.id var dune: Book = add_book("Dune", "Back room") var stock: Response = request { method: HttpMethod.get path: "/books/stock" as: Principal.user(ann) } var shelved: StockBook = StockBook { id: dune.id title: "Dune" shelf_note: "Back room" } assert stock.output == [shelved] var refused: Response = request { method: HttpMethod.get path: "/books/stock" as: Principal.user(cyd) } assert refused.status == 403 assert refused.code == "not_permitted" var browsed: Response = request { method: HttpMethod.get path: "/books" as: Principal.user(cyd) } var on_shelf: CatalogueBook = CatalogueBook { id: dune.id title: "Dune" } assert browsed.output == [on_shelf]}test "a role removed is refused on the next request" { var ann: Member = attempt register("ann") var staff: Staff = attempt appoint(ann.id) var dune: Book = add_book("Dune", "Back room") var before: Response = request { method: HttpMethod.get path: "/books/stock" as: Principal.user(ann) } assert before.status == 200 attempt stand_down(staff.id) var after: Response = request { method: HttpMethod.get path: "/books/stock" as: Principal.user(ann) } assert after.status == 403 assert dune.title == "Dune"}
Markdown for agents: /docs/action-permissions.md.Built from Jadpo main at 7b64d64.