feat(security): DashboardTags on API-key constraints; sessions inherit owner tags (SEC-25 groundwork)

Adds a dashboard event-visibility tag to ApiKeyConstraints, riding in the existing
constraints JSON blob so no auth-store schema migration is needed (design
docs/plans/2026-07-10-dashboard-session-acl-tst15.md sections 3/3.1, open call
settled per its own recommendation). The tag is visibility-only: no read, write,
browse, or subscribe path consults it, and HasRead/HasWriteConstraints ignore it.

GatewaySession gains an immutable, ordinal-ignore-case Tags set stamped at
construction from the owning API key, forwarded by MxAccessGatewayService.OpenSession
from the resolved ApiKeyIdentity — never from the wire request, so a client cannot
label its own session with another tenant's tag. ISessionManager gains a tag-carrying
OpenSessionAsync overload whose default implementation forwards to the tagless one, so
an implementation that does not model tags opens an untagged (least visible) session.

apikey create-key gains --dashboard-tags team-a,team-b (repeatable, trimmed,
de-duplicated; an empty segment is rejected rather than dropped) and list-keys prints
the tags column. No enforcement yet — the EventsHub ACL that consumes the tag is a
later change.
This commit is contained in:
Joseph Doherty
2026-08-17 03:58:14 -04:00
parent 9130994736
commit a212e145ac
14 changed files with 477 additions and 10 deletions
+15 -2
View File
@@ -263,6 +263,7 @@ mxgateway apikey init-db
mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes read,write
mxgateway apikey create-key --key-id area1.reader --display-name "Area 1 reader" --scopes invoke:read,metadata:read --read-subtree "Area1/*" --browse-subtree "Area1/*"
mxgateway apikey create-key --key-id ops.temp --display-name "Temp contractor" --scopes invoke:read --expires 90d
mxgateway apikey create-key --key-id team-a.svc --display-name "Team A service" --scopes session:open,invoke:read --dashboard-tags team-a
mxgateway apikey create-key --key-id ops.audit --display-name "Audit window" --scopes metadata:read --expires 2027-01-01T00:00:00Z
mxgateway apikey list-keys --json
mxgateway apikey revoke-key --key-id ops.alice
@@ -272,8 +273,20 @@ mxgateway apikey rotate-key --key-id ops.alice
Constraint flags are optional. `--read-subtree`, `--write-subtree`,
`--read-tag-glob`, `--write-tag-glob`, and `--browse-subtree` are repeatable.
`--max-write-classification` accepts one integer. `--read-alarm-only` and
`--read-historized-only` are boolean flags. Existing rows with null constraints
remain fully unconstrained after migration.
`--read-historized-only` are boolean flags. `--dashboard-tags` takes a
comma-separated list (`--dashboard-tags team-a,team-b`) and is repeatable; its
segments are trimmed and de-duplicated ordinal-ignore-case, and an empty segment
is rejected rather than dropped so a stray comma cannot silently persist a grant
the operator did not write. Existing rows with null constraints remain fully
unconstrained after migration; rows written before `--dashboard-tags` existed
deserialize as untagged, unchanged in every other respect.
`--dashboard-tags` is *not* a data-access constraint — it only labels the key for
dashboard event visibility, and sessions the key opens inherit it. See
[Authorization](./Authorization.md#constraint-enforcement).
`list-keys` prints the tags as a trailing tab-separated column (`-` when
untagged); the values are operator-chosen labels, not key material.
Key ids are restricted by the parser to ASCII letters, digits, periods, and hyphens
so they remain safe to embed in the token format and in URL paths used by
+24
View File
@@ -178,6 +178,30 @@ Supported constraints are:
| `browse_subtrees` | Contained-path globs used to filter Galaxy browse results and deploy-event counts. |
| `read_alarm_only` | Read/subscription commands must target objects with alarm-bearing attributes. |
| `read_historized_only` | Read/subscription commands must target objects with historized attributes. |
| `dashboard_tags` | Dashboard event-visibility tags. **Not a data-access constraint** — see below. |
`dashboard_tags` is the one member of the blob that constrains nothing on the
gRPC data path. No read, write, browse, or subscribe check consults it, and
`HasReadConstraints` / `HasWriteConstraints` deliberately ignore it: adding a tag
neither widens nor narrows what a key may read or write. It rides in the same
serialized blob only to avoid an auth-store schema migration
(`docs/plans/2026-07-10-dashboard-session-acl-tst15.md` §3.1).
Its sole purpose is dashboard event visibility. A session records the tags of the
API key that opened it (`GatewaySession.Tags`, immutable for the session's life,
compared ordinal-ignore-case). The tags come from the owning key, never from the
client's `OpenSession` request, so a client cannot label its own session with
another tenant's tag. A key with no tags opens untagged sessions.
Tags are set at key creation with
`apikey create-key --dashboard-tags team-a,team-b` (repeatable; segments are
trimmed and de-duplicated ordinal-ignore-case). Keys created from the dashboard
API Keys page are currently always untagged.
The tag is carried end to end today; the dashboard ACL that consumes it — scoping
a Viewer's `EventsHub` subscriptions to the sessions their LDAP groups are
granted — is a separate change. Until it lands, the tag affects nothing at
runtime.
Glob matching is anchored, case-insensitive, and supports `*` and `?`.
Subtree and tag glob lists are alternatives: matching either list allows that