Skip to content

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 operator and admin today — 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

  1. 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.
  2. Capabilities — the shared pivot. A granular vocabulary of actions (author_plan, create_voice, nominate_target, …), defined once in common. Roles never gate features directly — they resolve to capabilities, and capabilities are what every client checks.
  3. 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 operator means J6 / signaler, not a generic field user — on Android it confers channel-owner powers. This model moves J6 to a new signals role and reuses operator for 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 in testdata/authorization_vectors.json) as of common#90. This table mirrors them — common wins 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 a field-origin entry into the registry (owner = the creating principal); author_comms_plan = author / bless planned-origin entries. A signaller creating a channel "on the hoof" flows back into the plan as a provisional field entry 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 granted operator/admin a blanket override and folded device tasking + annotation moderation onto manage_members in clients. As implemented, those were broken out into the first-class command_device_camera, command_device_movement, and manage_annotations capabilities with the grants above. common's testdata/authorization_vectors.json is 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_voice gates 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:

  1. common — ✅ done (common#90): the Capability enum + per-role defaultCapabilities + the expanded role names (operator = field user, new signals J6), with golden vectors. Adds device-tasking (command_device_camera / command_device_movement) and manage_annotations (moderation) beyond the original list.
  2. Directory — issue the expanded roles in the IdentityToken; admin surface to assign roles (assign signals to J6 personnel when standing up).
  3. web — resolve roles→capabilities, server capability middleware on mutation routes, client capability-gated tabs/controls; migrate off the bare operator/admin assumption.
  4. Android — its capability → action mapping (e.g. who may create voice).
  5. 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, channel owner as 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.