Authorization model¶
Status: Decision (proposed) — 2026-06-15. Captures the agreed shape; each repo implements its slice separately (see Sequencing).
This is authorization (who may do what), distinct from authentication (who you are) and the per-message identity/confidentiality gates in Security model.
Decision, in one line¶
Expand the role set carried in the Directory IdentityToken (today just
operator / admin); resolve those global roles through a shared
capability vocabulary; each client enforces capabilities in the surfaces
that make sense for it. A single default role→capability mapping ships first;
per-operation overrides ("what a planner means in this op") are designed for
but deferred.
Why¶
- The IdentityToken carries only
operatorandadmintoday — too coarse to express "this person plans, that person targets, that person runs comms". - There is no shared vocabulary, so the same question ("may they create a voice channel?") is answered differently — or not at all — per platform.
- Web enforcement is membership-only server-side (an observer can call mutation APIs directly); the role barely gates anything.
The three pieces¶
- Roles — global, in the token. Directory issues a principal's roles in the IdentityToken (per-principal, apply everywhere). Operation membership decides which operations you enter; your roles decide what you can do in them.
- Capabilities — the shared pivot. A granular vocabulary of actions
(
author_plan,create_voice,nominate_target, …), defined once incommon. Roles never gate features directly — they resolve to capabilities, and capabilities are what every client checks. - Role → capability mapping. One default mapping, shared. Resolved per
(operation, role)so a future per-operation override can change what a role grants in a given op — today that resolution simply returns the default.
flowchart LR
R[token roles<br/>planner, signals] --> M[role→capability<br/>default mapping<br/>per-op override = future]
M --> C[capabilities<br/>author_plan, create_voice, ...]
C --> W[Web: routes / tabs]
C --> A[Android: actions<br/>e.g. create voice]
effectiveCapabilities(principal, op) = ⋃ over token.roles of roleCapabilities(op, role)
where roleCapabilities(op, role) today = DEFAULT[role], and later =
DEFAULT[role] overridden by the operation's config.
Role vocabulary (global, token-issued)¶
| Role | Intent |
|---|---|
observer |
Monitor only — read/listen, no writes or transmit |
operator |
(repurposed) Lightweight field user — transmit voice/chat, map drawings |
signals |
J6 / CIS — runs comms: owns channels + the comms plan (this is today's operator) |
planner |
Build and publish the future-state plan (PLAN) |
targeter |
Run the targeting workflow |
admin |
Manage operation members + roles; superset (existing) |
A principal may hold several (e.g. [planner, signals]). Additive — new
functions add a role.
Naming: today's token
operatormeans J6 / signaler, not a generic field user — on Android it confers channel-owner powers. This model moves J6 to a newsignalsrole and reusesoperatorfor the field-user role. Cutover is on shipping this work (no fielded deployments to migrate) — see Naming. The J6 enum value is a single token,signals(NATO J6 convention); "Comms / Signals" is only a display label.
Capability vocabulary (shared, in common)¶
Authoritative source: the enum + per-role defaults live in
common(proto/server/authorization.proto,ts/src/authz.ts, golden vectors intestdata/authorization_vectors.json) as of common#90. This table mirrors them —commonwins on any drift.
| Domain | Capability | Meaning |
|---|---|---|
| View | view_operation |
Read COP + REPLAY; read-only PLAN / TARGETING |
| Comms | create_voice |
Create a voice channel |
| Comms | create_chat |
Create a chat channel |
| Comms | manage_channels |
Channel-owner powers: edit / delete / re-own channels and channel membership (invite / kick) |
| Comms | transmit_voice |
Speak on voice (PTT) |
| Comms | send_chat |
Send chat messages |
| COP | draw_annotations |
Create / edit your own map drawings |
| COP | manage_annotations |
Edit / delete any drawing (moderation override); draw_annotations covers only your own |
| Plan | author_plan |
Build PLAN layers / sections |
| Plan | publish_plan |
Release planned content to the published set |
| Targeting | nominate_target |
Nominate a target |
| Targeting | manage_targets |
Manage the targeting board (status, effects) |
| Comms-plan | author_comms_plan |
Author the comms matrix (nets → voice+chat) |
| COP | manage_annotations |
Edit / delete any drawing (moderation override) — draw_annotations covers only your own |
| Tasking | command_device_camera |
Task another node's camera / sensor (ROI, pan/tilt, zoom, capture) |
| Tasking | command_device_movement |
Task a node's movement / posture (move, recall/hold) — higher kinetic-risk tier |
| Admin | manage_members |
Assign operation members + roles (distinct from channel membership, which is under manage_channels) |
| Device | command_device_camera |
Task another node's camera / sensor: ROI, pitch/yaw, zoom, capture |
| Device | command_device_movement |
Task a node's movement / posture: MoveOrder, Recall/Hold. Higher (kinetic) risk tier than camera tasking |
Convergence (forward-compatible meaning). The comms plan is the intended authoritative, signed, gossiped channel registry for an operation; live voice/chat channels are a projection of it, not a separate system. So:
create_voice/create_chat= create a live channel, i.e. write afield-origin entry into the registry (owner = the creating principal);author_comms_plan= author / blessplanned-origin entries. A signaller creating a channel "on the hoof" flows back into the plan as a provisionalfieldentry that signals can later formalize. Decoupled today (comms-net is web-local, realtime publication deferred); these capabilities are defined so the convergence target is not contradicted. The registry schema, signing / merge rules, and offline-merge policy are a separate design (see Out of scope).
Default role → capability mapping¶
The shared default (overridable per operation later). A principal's effective capabilities are the union across their roles.
Per common#90's defaultCapabilities (golden-vectored):
| Role | Capabilities |
|---|---|
observer |
view_operation |
operator (field user) |
view_operation transmit_voice send_chat draw_annotations command_device_camera |
signals (J6) |
view_operation manage_channels create_voice create_chat author_comms_plan transmit_voice send_chat draw_annotations manage_annotations command_device_camera command_device_movement |
planner |
view_operation author_plan publish_plan draw_annotations manage_annotations |
targeter |
view_operation nominate_target manage_targets draw_annotations manage_annotations command_device_camera command_device_movement |
admin |
all 16 capabilities (incl. manage_members) |
signals (J6) owns comms — channels (incl. channel membership, via
manage_channels) and the comms plan — and can transmit. It does not get
manage_members (operation roles); that is admin only. operator (field
user) is lightweight: transmit + draw + camera tasking, no channel ownership and
no movement tasking (a higher kinetic-risk tier reserved for signals / targeter
/ admin).
Annotation moderation. draw_annotations is scoped to a principal's
own drawings; manage_annotations (edit/delete any drawing) is the
moderation override. The supervisory roles — signals, planner, targeter
(and admin) — hold manage_annotations, and also carry draw_annotations
so a moderator can author, not only delete. The lightweight operator draws
its own annotations but cannot moderate; observer gets neither.
Device tasking. command_device_camera and command_device_movement are
distinct risk tiers. Camera tasking goes to operator, signals, targeter
(and admin). Movement/posture tasking (kinetic) is withheld from the
lightweight operator — only signals, targeter, and admin hold
command_device_movement. observer (monitor-only) gets neither: tasking a
device is a control write, incompatible with read-only.
Implementation note (shipped in
common). The first cut of this table grantedoperator/admina blanket override and folded device tasking + annotation moderation ontomanage_membersin clients. As implemented, those were broken out into the first-classcommand_device_camera,command_device_movement, andmanage_annotationscapabilities with the grants above.common'stestdata/authorization_vectors.jsonis the authoritative pin; this table mirrors it.
Naming¶
operator's meaning changes from today's J6 / signaler to the new
lightweight field user; J6 moves to the new signals role.
Cutover is on shipping this work — there are no fielded deployments to
migrate, so there is no alias window, token drain, or re-grant of in-flight
tokens to worry about. Roles are simply defined and assigned: give J6 personnel
the signals role when standing up an operation. The J6 enum value is a single
token, signals (NATO J6); "Comms / Signals" is display only.
Per-operation override (future)¶
The resolver is keyed on (operation, role) precisely so an operation can later
redefine a role — e.g. in one operation a planner also nominates targets, in
another it does not. Not built now: today every operation resolves to the
default above. The design simply leaves the seam (the resolver signature, and an
operation-scoped role-definition store/admin surface) so adding overrides does
not change call sites.
Each client maps capabilities → its surfaces¶
Capabilities are the shared contract; turning a capability into a concrete gate is per client:
- Web — server-side route middleware keyed on the required capability (operation-scoped mutation routes gain a capability guard, replacing membership-only gating); client tab/control visibility keyed on the effective capabilities surfaced in shared props. Server authoritative; client a UX mirror.
- Android — capability gates at its command/transport/UI layers (e.g.
create_voicegates the create-voice action). The channel-definition persistence gate already enforces owner/admin on the receive side (see Security model).
Per-client subset. A client enforces only the capabilities for features it
has; capabilities for surfaces it lacks are simply never checked. Android today
gates view_operation, transmit_voice, send_chat, draw_annotations,
create_voice, create_chat, manage_channels, manage_members;
author_plan / publish_plan / nominate_target / manage_targets exist in
the shared enum but have no Android surface yet, so planner / targeter
degrade cleanly to observer there. Same enum everywhere — each client a
subset.
A single helper (hasCapability(effective, cap)) is the only check sites call.
Entry & holding¶
- Login eligibility (now): any Directory principal who completes the webauth exchange and is not disabled may obtain a session. No app-level allowlist yet. Future: gate login on a role/clearance issued by Directory.
- Unassigned: a logged-in principal who is not a member of any operation sees the holding page ("you have not been assigned to an operation").
- In an operation: capabilities = the resolved mapping of the principal's global roles; a member whose roles resolve to nothing actionable is, in effect, read-only.
Sequencing (multi-repo)¶
Each behind its own PR:
common— ✅ done (common#90): theCapabilityenum + per-roledefaultCapabilities+ the expanded role names (operator= field user, newsignalsJ6), with golden vectors. Adds device-tasking (command_device_camera/command_device_movement) andmanage_annotations(moderation) beyond the original list.- Directory — issue the expanded roles in the IdentityToken; admin surface
to assign roles (assign
signalsto J6 personnel when standing up). - web — resolve roles→capabilities, server capability middleware on
mutation routes, client capability-gated tabs/controls; migrate off the bare
operator/adminassumption. - Android — its capability → action mapping (e.g. who may create voice).
- Later — per-operation role-definition overrides (store + admin surface + resolver wiring).
Forward-compatible: until a client adds its mapping it falls back to current behaviour; an unknown role grants nothing extra.
Out of scope¶
- Comms-plan ↔ live-channel convergence — making the comms plan the signed,
gossiped channel registry with
origin: planned | field, field channels merging back as provisional entries, channelowneras a registry field, and the offline-creation merge rule (two devices independently minting "Net Alpha"). This is the promoted "comms-net realtime publication" roadmap line; it gets its own design and the genuinely hard distributed-systems decision (signals-only resolves vs last-writer-wins by sequence). This document only fixes the forward-compatible meaning of the comms capabilities (see the Convergence note above) so that work is not contradicted. - Per-operation role scoping — different roles per operation. Roles are global for now (see Per-operation override). Note the per-entry channel-admin grant the convergence work needs is the same mechanism.
Relationship to the per-message gates¶
This decides whether an actor may attempt an action. The channel-definition integrity gates and the per-message identity/classification checks remain the last line on the receive side — even an authorized actor's frames are verified and the classification ceiling still attaches. Authorization at the source, crypto gates at the sink.