Kanban Authorization
kanban_move? policy predicate
Every drag-and-drop move is authorized through the kanban_move? method on the resource's policy. The default implementation delegates to update?:
# Plutonium::Resource::Policy — built-in default
def kanban_move?
update?
endOverride it in your policy to give finer control:
class TaskPolicy < ResourcePolicy
# Allow all authenticated members to drag cards,
# but require :admin to open the edit form.
def kanban_move?
user.member?
end
def update?
user.admin?
end
endGating a specific transition (from / to context)
kanban_move? is the single authorization for every move — plain moves and enter_interaction: columns alike. To gate a specific transition, read the source and destination columns from the authorization context. They are exposed as the optional kanban_from / kanban_to policy readers (the Plutonium::Kanban::Column objects), and are nil for every non-move authorization:
class DealPolicy < ResourcePolicy
# Anyone on the team may shuffle cards, but only a manager may move one
# INTO "Closed Won".
def kanban_move?
return user.manager? if kanban_to&.key == :closed_won
super
end
endRules take no positional arguments in ActionPolicy — the columns arrive via context, which the controller supplies on the kanban_move? check (context: { kanban_from:, kanban_to: }). This replaces per-column policy methods: an enter_interaction: column is authorized by kanban_move? too, so there is no separate mark_lost?-style predicate to define.
Both kanban_from and kanban_to are trustworthy to authorize on. to is where the card ends up; and although from_column arrives from the client, the move handler verifies the record actually resides in the claimed source column before it proceeds (a mismatch snaps the drag back), so a spoofed or stale from can never drive a move past a kanban_from-based rule.
Read-only board
When kanban_move? returns false for the current user, the board is rendered read-only. Cards are displayed but dragging is disabled — no drag handles appear and the Stimulus controller does not register drop zones.
Authorization flow on a move
When a card is dropped, the server:
- Finds the record within the current authorized scope (the same policy
relation_scopeused by the index action). - Calls
authorize_current!(record, to: :kanban_move?, context: { kanban_from:, kanban_to: })— the single authorization for the move (anenter_interaction:column rides on this same check, with no policy method of its own). Afalseresult halts the action with HTTP 403. - Verifies the record actually resides in the claimed source column (
from_columnis client-supplied). A mismatch responds 422 and snaps the card back — this is what makeskanban_fromsafe to authorize on. - Validates the drop against the destination column's
accepts:policy andlocked:flag. A rejected drop responds with HTTP 422 and re-renders the source column (the Stimulus controller snaps the card back). - Enforces the destination column's
wip:limit (cross-column moves only). Exceeding the WIP cap also responds 422. - Calls
on_enterand repositions the record inside a transaction.
No permitted attributes for moves
Kanban moves do not pass through permitted_attributes_for_update / permitted_attributes_for_kanban_move. The on_enter callback is author code that runs with full model access — it is the responsibility of the on_enter implementation to assign only the attributes appropriate for a column transition. This is intentional: the callback is trusted Ruby, not user-supplied form data.
Column-level drop policies
The accepts:, locked:, and wip: column options enforce additional constraints beyond kanban_move?:
| Constraint | What it checks | On failure |
|---|---|---|
accepts: | Source column key is allowed | 422 + card snap-back |
locked: | Source column is not locked | 422 + card snap-back |
wip: | Cross-column count within limit | 422 + card snap-back |
These checks run server-side after kanban_move? succeeds. The client-side Stimulus controller reads data-kanban-accepts and data-kanban-locked attributes to provide visual drop hints, but the server remains the authority.
Quick-add authorization
The + Add button (shown when add: true is set on a column) is only rendered when the current policy's create? returns true. The new form opened by quick-add is the standard resource new form and goes through the normal creation authorization flow.
