generated from coulomb/repo-seed
Compare commits
3 Commits
52b5575048
...
f930e96568
| Author | SHA1 | Date | |
|---|---|---|---|
| f930e96568 | |||
| 7471e07cbb | |||
| 82177d88a9 |
164
docs/iam-profile-consumption.md
Normal file
164
docs/iam-profile-consumption.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# NetKingdom IAM Profile — flex-auth Consumption Surface
|
||||
|
||||
Date: 2026-05-16
|
||||
Status: Draft for FLEX-WP-0005 P5.5; binds the input contract for the
|
||||
standalone evaluator (FLEX-WP-0002) and every PDP adapter (FLEX-WP-0004).
|
||||
Upstream: `~/the-custodian/canon/standards/iam-profile_v0.1.md`.
|
||||
|
||||
## Boundary
|
||||
|
||||
The NetKingdom IAM Profile defines the OIDC contract shared across hubs
|
||||
and services. flex-auth **consumes verified claims**; it does not
|
||||
verify token signatures, fetch JWKS, or terminate OIDC sessions. Those
|
||||
responsibilities belong upstream:
|
||||
|
||||
- **key-cape (lightweight mode)** validates tokens against its local
|
||||
OIDC provider and emits claims that conform to the profile.
|
||||
- **Keycloak (heavy mode)** signs tokens; integration code (e.g.
|
||||
Markitect's `NetKingdomIdentityClaimsAdapter`) validates issuer,
|
||||
audience, signature, expiry, and clock skew before handing claims to
|
||||
flex-auth.
|
||||
|
||||
A flex-auth deployment that exposes a network endpoint MUST be fronted
|
||||
by an identity layer that does the verification. The flex-auth core
|
||||
accepts a normalized claim envelope and is responsible for everything
|
||||
*after* "this caller is authenticated".
|
||||
|
||||
## Input Envelope
|
||||
|
||||
flex-auth's standalone evaluator and adapters consume a normalized
|
||||
envelope identical to Markitect's `EnterpriseIdentity` shape:
|
||||
|
||||
```yaml
|
||||
issuer: <oidc issuer URL> # required
|
||||
subject: <stable subject id> # required
|
||||
principal_type: human | service | emergency
|
||||
audience: [<aud>, ...] # required, non-empty
|
||||
authorized_party: <azp or client_id, optional>
|
||||
preferred_username: <string> # required for humans
|
||||
roles: [<role>, ...] # required, non-empty
|
||||
scopes: [<scope>, ...] # required, non-empty
|
||||
groups: [<group>, ...] # optional; resolved by directory layer
|
||||
assurance:
|
||||
acr: <oidc acr value, optional>
|
||||
amr: [<oidc amr value>, ...] # e.g. pwd, otp, mfa, hwk
|
||||
mfa: <bool — derived from amr>
|
||||
directory:
|
||||
groups_claim_present: <bool>
|
||||
group_overage: <bool> # Microsoft Entra-style group overage
|
||||
claims: { ... } # full original claim map (minus 'groups')
|
||||
provenance:
|
||||
source: claims | jwt | jwt-fixture
|
||||
verified_signature: <bool>
|
||||
```
|
||||
|
||||
This is the envelope every check API call receives, regardless of
|
||||
which upstream identity provider produced the token.
|
||||
|
||||
## Required Claims (per IAM Profile §"Required Claims")
|
||||
|
||||
flex-auth treats the following as hard requirements. Missing any
|
||||
produces a `validation_error` before the request reaches a policy
|
||||
package.
|
||||
|
||||
| Claim | flex-auth field | Notes |
|
||||
| --- | --- | --- |
|
||||
| `iss` | `issuer` | Must match the deployment's expected issuer; production rejects local-dev issuers (`localhost`, `127.0.0.1`, `.local`, `dev.local`). |
|
||||
| `sub` | `subject` | Stable identifier; not a username. |
|
||||
| `aud` | `audience` | Must include the flex-auth instance or the protected system. |
|
||||
| `exp` | (validated upstream) | flex-auth tolerates ≤60s clock skew per profile §"Token Lifecycle". |
|
||||
| `iat` | (validated upstream) | Same. |
|
||||
| `scope` or `scp` | `scopes` | At least one scope required. Empty scope is a hard fail. |
|
||||
| `preferred_username` | `preferred_username` | Required for `principal_type=human`. Optional for service accounts. |
|
||||
| `roles` or `realm_access.roles` or `resource_access.<client>.roles` | `roles` | Union of all three sources. At least one role required. |
|
||||
|
||||
## Recommended Claims
|
||||
|
||||
| Claim | flex-auth field | Use |
|
||||
| --- | --- | --- |
|
||||
| `email` | `claims.email` | Contact identity; **never** used for authorization decisions. |
|
||||
| `name` | `claims.name` | Display only. |
|
||||
| `groups` | `groups` (after resolution) | Authorization input; subject to freshness/overage. |
|
||||
| `azp` | `authorized_party` | Distinguishes service-account client from impersonating client. |
|
||||
| `acr` | `assurance.acr` | Authentication context class; gates high-trust scopes. |
|
||||
| `amr` | `assurance.amr` | Authentication methods; `otp`/`mfa`/`hwk` lift `assurance.mfa` to true. |
|
||||
|
||||
## Tolerated Variations
|
||||
|
||||
flex-auth normalizes — protected systems never see the variation.
|
||||
|
||||
- **Role claim location.** Three OIDC providers ship roles in three
|
||||
places: top-level `roles`, Keycloak's `realm_access.roles`, and
|
||||
Keycloak's per-client `resource_access.<client>.roles`. flex-auth
|
||||
unions all three.
|
||||
- **Scope encoding.** `scope` (space-separated string) and `scp`
|
||||
(array) both accepted; both produce the same `scopes` array.
|
||||
- **Audience encoding.** `aud` as a single string or as an array;
|
||||
flex-auth always normalizes to an array.
|
||||
- **MFA signal.** Either an explicit `mfa: true` claim or any of
|
||||
`otp`/`mfa`/`hwk` in `amr` produces `assurance.mfa = true`.
|
||||
|
||||
## Principal-Type Detection
|
||||
|
||||
flex-auth classifies the principal by:
|
||||
|
||||
1. If `client_id` is set and `service` is in `roles` → `service`.
|
||||
2. If `azp` starts with `svc-` or `service` is in `roles` → `service`.
|
||||
3. If `emergency` is in `roles` → `emergency`.
|
||||
4. Otherwise → `human`.
|
||||
|
||||
This matches Markitect's `NetKingdomIdentityClaimsAdapter._principal_type`
|
||||
and follows IAM Profile §"Hub-to-Hub Service Account Pattern" (service
|
||||
accounts named `svc-*` and carrying the `service` role).
|
||||
|
||||
## Group Overage and Freshness
|
||||
|
||||
Microsoft Entra and Keycloak both clip the `groups` claim once a
|
||||
threshold is reached; the token then carries `hasgroups: true` (Entra)
|
||||
or `_claim_names.groups` (also Entra). flex-auth's directory layer is
|
||||
responsible for resolving the full set via Graph/SCIM/Keycloak admin
|
||||
API; the claim envelope carries `directory.group_overage = true` so
|
||||
policy packages can decide whether to fail-closed or accept the
|
||||
partial set with an `audit_only` outcome.
|
||||
|
||||
Group freshness is tracked at the directory-resolver layer (out of
|
||||
scope for this document; see FLEX-WP-0004 T05).
|
||||
|
||||
## Production vs Local Development
|
||||
|
||||
Per IAM Profile §"Local Development Profile":
|
||||
|
||||
- Local-development issuers (`localhost`, `127.0.0.1`, hostnames
|
||||
ending in `.local`, `dev.local`) are rejected when
|
||||
`environment=production` is set in the request context.
|
||||
- A development token marked clearly through issuer/audience is
|
||||
accepted in non-production environments.
|
||||
- The local-development path exists to keep flex-auth useful before
|
||||
Keycloak is wired in; it never weakens production rules.
|
||||
|
||||
## Emergency Principals
|
||||
|
||||
Per IAM Profile §"Human Override and Emergency Access":
|
||||
|
||||
- `emergency` is a first-class `principal_type`.
|
||||
- Every decision involving an emergency principal MUST record a
|
||||
`record_emergency` obligation in the decision envelope.
|
||||
- Policy packages MAY allow emergency principals; flex-auth's audit
|
||||
layer ensures the action is durable regardless.
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
Markitect's `NetKingdomIdentityClaimsAdapter` (at
|
||||
`markitect-tool/src/markitect_tool/policy/enterprise.py`) implements
|
||||
the validation steps above in Python. flex-auth's Go implementation
|
||||
(FLEX-WP-0002 P2.4) mirrors its behavior and stays in sync via
|
||||
contract tests against the fixtures in `examples/claims/`.
|
||||
|
||||
## Open Items
|
||||
|
||||
- Whether `roles` becomes canonical and `realm_access.roles` becomes
|
||||
legacy is still listed as an open question in IAM Profile v0.1. As
|
||||
of 2026-05-16 flex-auth normalizes both with no preference.
|
||||
- `Workload identity` (Kubernetes service-account tokens, GCP/AWS
|
||||
workload-identity federation) is not yet in the IAM Profile.
|
||||
flex-auth's service-account handling is currently OIDC-only.
|
||||
306
docs/topaz-mapping-spike.md
Normal file
306
docs/topaz-mapping-spike.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Topaz Alignment Spike
|
||||
|
||||
Date: 2026-05-16
|
||||
Status: Spike output for FLEX-WP-0005 P5.4. Feeds FLEX-WP-0002 P2.1
|
||||
(schemas) and FLEX-WP-0004 T01 (Topaz adapter).
|
||||
Author: claude with Bernd
|
||||
|
||||
## Goal
|
||||
|
||||
Validate ADR-003's commitment to shape the flex-auth standalone core so
|
||||
that the Topaz adapter is a small step. Concretely: confirm that
|
||||
flex-auth's resource, subject, group, team, and relationship vocabulary
|
||||
can map cleanly onto Topaz directory objects and relations, and that
|
||||
flex-auth's Rego-in-Markdown policy packages decompose to Topaz policy
|
||||
modules without translation.
|
||||
|
||||
## Topaz In Two Paragraphs
|
||||
|
||||
Topaz bundles three things behind one process: an OPA Rego evaluator,
|
||||
an embedded Zanzibar-style directory ("EDS" — Edge Authorization
|
||||
Database), and a gRPC/HTTP API that ties them together. The directory
|
||||
stores objects (typed entities) and relations (typed edges between
|
||||
objects). A `manifest` declares the object types, relation types, and
|
||||
permission expressions. Policy modules in Rego consult both the request
|
||||
input *and* directory data (via builtins like `ds.check`,
|
||||
`ds.object`, `ds.relation`) to produce a decision.
|
||||
|
||||
Topaz exposes four service surfaces: **reader** (read directory),
|
||||
**writer** (mutate directory), **model** (manage manifest), and
|
||||
**authorizer** (evaluate policy). The `authorizer.Is` and
|
||||
`authorizer.DecisionTree` RPCs are the primary integration points for a
|
||||
policy decision point.
|
||||
|
||||
## Vocabulary Map
|
||||
|
||||
flex-auth's canonical vocabulary maps onto Topaz as follows.
|
||||
|
||||
| flex-auth concept | Topaz representation | Notes |
|
||||
| --- | --- | --- |
|
||||
| Subject (human) | `object{type:"user", id:<oidc subject>}` | `display_name` from `preferred_username` or claims |
|
||||
| Subject (service account) | `object{type:"user", id:<sub>}` with `properties.principal_type:"service"` | Same type; principal_type carried in properties |
|
||||
| Group | `object{type:"group", id:<group slug>}` | Members via `member` relation |
|
||||
| Team | `object{type:"group", id:"team:<slug>"}` | Modeled as a kind of group; slug-prefixed for clarity |
|
||||
| Tenant | `object{type:"tenant", id:<slug>}` | Resources reference tenant via `tenant` relation |
|
||||
| Group membership | `relation{subject:user, name:"member", object:group}` | Recursive group→group also supported |
|
||||
| Resource (Markitect knowledge_base / document / …) | `object{type:<flex_auth_type>, id:<resource id>}` | Type names match `FlexAuthResource.type` exactly |
|
||||
| Resource parent | `relation{subject:child, name:"parent", object:parent}` | Walks inheritance |
|
||||
| Resource owner team | `relation{subject:group, name:"owner_team", object:resource}` | Group-on-resource |
|
||||
| Resource labels | `object.properties.labels: [...]` | Carried as JSON properties; consulted by Rego |
|
||||
| Resource trust_zone | `object.properties.trust_zone: <str>` | Same |
|
||||
| Action | Topaz permission name | `read`, `query`, `search`, `package`, `export`, `workflow_run`, `admin` |
|
||||
| Relationship fact (subject↔resource) | `relation{subject, name:<relation>, object:resource}` | e.g. `reader`, `editor`, `steward` |
|
||||
| Policy package (Rego-in-Markdown) | One Rego module per package | Extracted from the Markdown by flex-auth's loader (ADR-002) |
|
||||
| Decision envelope | Topaz `authorizer.Is.Response` + Rego decision object | Topaz returns booleans per decision name; the Rego program returns the richer envelope and flex-auth wraps |
|
||||
| Policy version | `metadata.version` in the package frontmatter | Threaded as `input.policy.version` for Topaz; recorded in audit |
|
||||
| Provenance | `metadata` on Topaz objects/relations and on the decision | Topaz exposes `etag` for relations; flex-auth records both sides |
|
||||
|
||||
### Why these specific choices
|
||||
|
||||
- **Subject and service-account share `user` type.** Topaz's
|
||||
authorizer.Is treats `user` as the canonical identity subject. The
|
||||
`principal_type` distinction lives in `properties` so Rego rules
|
||||
can branch on it, but the directory shape stays uniform.
|
||||
- **Team modeled as group.** Markitect (and most consumers) treat
|
||||
teams as groups for authorization purposes. A separate `team` type
|
||||
is possible but adds an indirection. The `team:` id-prefix keeps
|
||||
semantics legible without forking the type.
|
||||
- **Labels and trust_zone in `properties` rather than as separate
|
||||
objects.** They are attributes of the resource, not first-class
|
||||
edges. Putting them in properties keeps the directory small and
|
||||
policies legible.
|
||||
- **Owner is a relation, not a property.** A team owns multiple
|
||||
resources; the inverse query "what does team T own?" is a relation
|
||||
walk, not a property scan.
|
||||
|
||||
## Rego Module Shape
|
||||
|
||||
A flex-auth Rego-in-Markdown package (ADR-002) extracts to a single
|
||||
Rego module. The same module runs in both flex-auth's embedded
|
||||
evaluator and in Topaz, because Topaz exposes the same `data.<package>`
|
||||
namespace and supports the same `ds.*` builtins flex-auth's adapter
|
||||
will mock when running standalone.
|
||||
|
||||
### Standalone-mode contract
|
||||
|
||||
When evaluated by the flex-auth standalone evaluator
|
||||
(`internal/policy`), the module sees:
|
||||
|
||||
```text
|
||||
input.subject # normalized subject (id, groups, roles, scopes, principal_type, assurance)
|
||||
input.action # action name
|
||||
input.resource # normalized resource (id, type, labels, trust_zone, owner, parent, …)
|
||||
input.context # request/environment/assurance/workflow attributes
|
||||
data.policy # registered package metadata table
|
||||
data.directory # standalone registry contents, exposed under the same shape ds.* would return
|
||||
```
|
||||
|
||||
The `ds.*` builtins are **shimmed locally** in standalone mode:
|
||||
flex-auth provides `ds.check`, `ds.object`, and `ds.relation`
|
||||
implementations that consult `internal/registry` instead of Topaz.
|
||||
The shim is a Go-side `rego.Function` registration.
|
||||
|
||||
### Delegated-mode contract
|
||||
|
||||
When evaluated by Topaz, the same module runs unchanged. `ds.*` calls
|
||||
hit Topaz's directory. The decision envelope returned to the protected
|
||||
system is identical because flex-auth's adapter wraps Topaz's response
|
||||
into the same `decision_envelope.schema.json` shape (FLEX-WP-0002
|
||||
P2.1).
|
||||
|
||||
### Decision object shape
|
||||
|
||||
The Rego package's `decision` rule produces:
|
||||
|
||||
```rego
|
||||
decision := {
|
||||
"effect": "allow" | "deny" | "redact" | "audit_only" | "not_applicable",
|
||||
"reason": "<rule id>",
|
||||
"obligations": [...], # optional, per ADR-002
|
||||
"diagnostics": {...}, # optional, free-form
|
||||
}
|
||||
```
|
||||
|
||||
flex-auth's adapter (standalone *or* Topaz) wraps this into the full
|
||||
decision envelope by adding subject metadata, resource metadata,
|
||||
matched policy version, matched rule, and provenance.
|
||||
|
||||
## Wire-Protocol Candidates
|
||||
|
||||
Ranked from primary to fallback:
|
||||
|
||||
1. **gRPC (Topaz's native API)** — primary. Type-safe, generated Go
|
||||
clients (`github.com/aserto-dev/topaz/pkg/cli` exposes the protos),
|
||||
low overhead. Authorizer.Is, DecisionTree, Reader/Writer/Model.
|
||||
2. **HTTP/REST gateway** — fallback for environments where gRPC is
|
||||
awkward (e.g. browser tooling, debugging). Topaz exposes a REST
|
||||
gateway on a sibling port; same RPCs, JSON-encoded.
|
||||
3. **Embedded library** — *not* recommended. Topaz's directory is
|
||||
tightly coupled to its process model and bolt/postgres storage;
|
||||
embedding it ties flex-auth's release cycle to Topaz's and erodes
|
||||
the adapter boundary. Reserved as a future optimization if a
|
||||
single-process deployment is ever required.
|
||||
|
||||
Adapter implementation under `internal/adapters/topaz/` consumes the
|
||||
gRPC client. The HTTP gateway is documented but not implemented in
|
||||
FLEX-WP-0004 T01.
|
||||
|
||||
## Adapter Surface (Preview For FLEX-WP-0004 T01)
|
||||
|
||||
```go
|
||||
package topaz
|
||||
|
||||
type Adapter interface {
|
||||
// Check evaluates a single decision against Topaz.
|
||||
Check(ctx context.Context, req CheckRequest) (DecisionEnvelope, error)
|
||||
|
||||
// BatchCheck evaluates multiple resources for one subject+action.
|
||||
BatchCheck(ctx context.Context, req BatchCheckRequest) ([]DecisionEnvelope, error)
|
||||
|
||||
// ImportDirectory writes flex-auth registry data into Topaz.
|
||||
ImportDirectory(ctx context.Context, snapshot DirectorySnapshot) error
|
||||
|
||||
// ImportPolicy installs a bundle (Rego modules extracted from
|
||||
// policy packages) into Topaz's OPA.
|
||||
ImportPolicy(ctx context.Context, bundle PolicyBundle) error
|
||||
}
|
||||
```
|
||||
|
||||
`DirectorySnapshot` and `PolicyBundle` are the canonical flex-auth
|
||||
types from `pkg/api/`; the adapter translates them into Topaz's
|
||||
proto types at the boundary.
|
||||
|
||||
## Recommendation: Schema Restatement, Not Embedding
|
||||
|
||||
flex-auth should **restate** its directory model in its own canonical
|
||||
schemas (FLEX-WP-0002 P2.1), not embed Topaz's directory proto.
|
||||
Reasoning:
|
||||
|
||||
- **The map is small.** flex-auth's vocabulary is six object kinds
|
||||
(user, group, tenant, resource, …) and a handful of relation
|
||||
conventions. The translation cost is tiny.
|
||||
- **Vocabulary divergence is likely.** flex-auth wants to record
|
||||
things Topaz's directory does not natively model — e.g. policy
|
||||
package version on a decision, IAM Profile assurance bundles,
|
||||
decision-time provenance. Embedding would force those into
|
||||
Topaz-shaped properties bags.
|
||||
- **Multi-backend goal.** OpenFGA and SpiceDB are next on the adapter
|
||||
list; both have similar-but-not-identical models. flex-auth's
|
||||
canonical schemas need to be the lingua franca, not Topaz's.
|
||||
- **Stable contract for Markitect.** Markitect already emits
|
||||
`FlexAuthResourceManifest`; that is the canonical input. Topaz is a
|
||||
consumer of the translation, not the other way around.
|
||||
|
||||
Adopt Topaz's manifest *style* (object types + relation types +
|
||||
permission expressions) for the standalone evaluator's directory
|
||||
shim, but keep flex-auth's own schema files as the source of truth.
|
||||
|
||||
## Runnable Example
|
||||
|
||||
`examples/topaz/` ships:
|
||||
|
||||
- `manifest.yaml` — Topaz v3 directory manifest declaring `user`,
|
||||
`group`, `tenant`, `knowledge_base`, `document` object types and
|
||||
relations `member`, `parent`, `owner_team`, `reader`, `steward`,
|
||||
plus the permission expressions for `read`, `query`, `search`,
|
||||
`export`, `admin`.
|
||||
- `policy/markitect.documents.rego` — Rego module matching the
|
||||
Rego-in-Markdown example in ADR-002 (internal document read gated
|
||||
on `reader` relation or `steward` role). This module shows the
|
||||
*flex-auth-shaped* input contract — bridging to Topaz's raw input
|
||||
is adapter scope (see Implementation Notes below).
|
||||
- `data/objects.json`, `data/relations.json` — seed directory data
|
||||
derived from `examples/markitect/resource_manifest.yaml` plus a
|
||||
small subject/group set.
|
||||
- `cfg/config.yaml` — Topaz v2 config (BoltDB at `/db`, TLS auto-
|
||||
managed in `/certs`, plaintext HTTP gateways for spike convenience,
|
||||
local bundle from `/bundle`).
|
||||
- `docker-compose.yml` — Topaz service + a one-shot `seed` container
|
||||
(pushes the manifest, objects, relations via the directory REST
|
||||
gateway) + a one-shot `probe` container that calls the directory
|
||||
`check` API for three scenarios.
|
||||
- `README.md` — usage.
|
||||
|
||||
**Verification status: green (2026-05-16).** `docker compose up
|
||||
--abort-on-container-exit` produced all three expected outcomes:
|
||||
|
||||
| Scenario | Subject | Resource | Permission | Expected | Actual |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| steward allow | alice (steward) | document:internal-note | read | true | true |
|
||||
| reader allow | bob (member of reader:platform-architecture) | document:internal-note | read | true | true |
|
||||
| outsider deny | eve (no relation) | document:internal-note | read | false | false |
|
||||
|
||||
## Implementation Notes Surfaced By The Spike
|
||||
|
||||
1. **Rego input contract bridging is adapter scope.** Topaz's
|
||||
authorizer delivers a fixed input shape
|
||||
(`input.user`, `input.identity`, `input.policy`, `input.resource`)
|
||||
that comes from its identity-resolution flow. flex-auth's
|
||||
policy packages use the canonical input shape from ADR-002
|
||||
(`input.subject`, `input.action`, `input.resource`, `input.context`).
|
||||
The Topaz adapter (FLEX-WP-0004 T01) is responsible for the wrapper
|
||||
that translates between the two. The standalone evaluator (P2.4)
|
||||
produces the canonical shape directly. **Implication for P2.1:**
|
||||
the canonical input shape must be locked early so both code paths
|
||||
target it.
|
||||
2. **Identity objects are Topaz-canonical, not flex-auth-canonical.**
|
||||
Topaz expects an `identity` object type with an `identifier`
|
||||
relation to a `user` object when `identity_context.type ==
|
||||
IDENTITY_TYPE_SUB`. flex-auth's subject model is flat (a Subject
|
||||
*is* the principal). The adapter materializes Topaz `identity`
|
||||
objects from the flex-auth subject registry at directory-import
|
||||
time, mapping subject id → identity → user. The standalone path
|
||||
doesn't need this indirection.
|
||||
3. **Permission resolution from the directory alone is sufficient
|
||||
for the common case.** The probe demonstrates that the Topaz
|
||||
manifest's permission expressions (`reader | steward | parent->read`
|
||||
etc.) resolve correctly with only directory data. Rego is only
|
||||
needed when policy decisions consume request context (assurance
|
||||
claims, time-of-day, trust_zone interaction) that the directory
|
||||
can't express. **Implication for P2.3/P2.4:** the policy package
|
||||
loader and check API should accept "permission-only" packages
|
||||
(no Rego rules, just a permission expression referencing the
|
||||
directory manifest) as a first-class case alongside Rego packages.
|
||||
4. **The Topaz config schema is not stable across versions.** Keys
|
||||
moved between `directory:` and `directory_service:`; relation
|
||||
rewrites use `->` only in `permissions:` not `relations:`;
|
||||
`disable_tls` is not honored as a server flag (use cert paths in
|
||||
a writable volume). The FLEX-WP-0004 T01 adapter implementation
|
||||
should pin a tested Topaz version and bump it deliberately.
|
||||
|
||||
## Implications for FLEX-WP-0002
|
||||
|
||||
- **P2.1 schemas** — define `directory_snapshot.schema.json` with the
|
||||
vocabulary above. Decision envelope unchanged from ADR-002 sketch.
|
||||
- **P2.2 registry** — store objects+relations in the same canonical
|
||||
shape; the in-memory representation is what gets translated to
|
||||
Topaz tuples at adapter time.
|
||||
- **P2.3 policy loader** — Markdown extractor produces *one* Rego
|
||||
module per package, identical for standalone and Topaz delivery.
|
||||
Standalone evaluator registers `ds.*` shim builtins.
|
||||
- **P2.4 check API** — `Check` returns the canonical envelope; the
|
||||
same code path serves standalone evaluation and Topaz-delegated
|
||||
evaluation; only the evaluator and `ds.*` resolution differ.
|
||||
|
||||
## Implications for FLEX-WP-0004
|
||||
|
||||
- **T01 Topaz adapter** — implement against the surface preview
|
||||
above; consume the proto client from `github.com/aserto-dev/topaz`.
|
||||
No new schema work; adapter is a translator and request/response
|
||||
shaper.
|
||||
- **T02 relationship PDP boundary** (OpenFGA, SpiceDB) — reuse the
|
||||
same `DirectorySnapshot` and decision envelope; only the directory
|
||||
protocol differs. The Topaz mapping above is reusable with small
|
||||
per-backend deltas.
|
||||
|
||||
## Open Items (Not Blocking P2.1)
|
||||
|
||||
- **Bundle distribution.** Topaz can load Rego from a remote bundle
|
||||
service (`opa.bundles`). Decision: load via local bundle path in
|
||||
the standalone deployment; switch to bundle service when running
|
||||
Topaz in clustered mode. Recorded for FLEX-WP-0004 T01.
|
||||
- **Consistency tokens.** Topaz's relation writes return an `etag`.
|
||||
Decision envelopes should carry it for read-your-writes guarantees;
|
||||
envelope field `provenance.directory_etag` is reserved for this.
|
||||
- **Group overage / freshness.** Out of scope for this spike; handled
|
||||
by directory resolver adapters in FLEX-WP-0004 T05.
|
||||
23
examples/claims/README.md
Normal file
23
examples/claims/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# examples/claims/
|
||||
|
||||
Contract fixtures for the NetKingdom IAM Profile v0.1 claim shapes
|
||||
flex-auth must accept. Each file is the *raw verified claim map* as
|
||||
flex-auth receives it from the upstream identity layer (key-cape or
|
||||
Keycloak); flex-auth's normalization produces the same
|
||||
`EnterpriseIdentity`-shaped envelope for all of them.
|
||||
|
||||
See `docs/iam-profile-consumption.md` for the full consumption
|
||||
surface.
|
||||
|
||||
| Fixture | Provider | Demonstrates |
|
||||
| --- | --- | --- |
|
||||
| `key-cape-lightweight.yaml` | key-cape lightweight mode | Profile-conformant minimum: single audience, top-level `roles` array, single-factor `amr=pwd`. |
|
||||
| `keycloak-heavy.yaml` | Keycloak production | Full variation set: `realm_access.roles` + `resource_access.<client>.roles`, scope as space-separated string, MFA via `amr=otp`, multiple audiences. |
|
||||
| `service-account.yaml` | Either provider | Hub-to-hub service account; `service` + `operator` roles, no `preferred_username`, narrow scope. |
|
||||
| `emergency.yaml` | Either provider | Break-glass human identity; `emergency` role, short expiry, hardware MFA, audit-trail metadata in an `emergency` claim. |
|
||||
| `keycloak-group-overage.yaml` | Entra/Keycloak | Group-claim overage signal (`hasgroups: true`); flex-auth's directory resolver fetches the full set. |
|
||||
|
||||
These fixtures are loaded by the standalone evaluator's contract tests
|
||||
(`FLEX-WP-0002 P2.4`) and by the Topaz adapter's contract tests
|
||||
(`FLEX-WP-0004 T01`). Both code paths MUST produce identical
|
||||
normalized envelopes for the same fixture.
|
||||
31
examples/claims/emergency.yaml
Normal file
31
examples/claims/emergency.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
# Claim envelope for an emergency (break-glass) human principal. Short
|
||||
# expiry, emergency role, requires MFA per the profile, and triggers
|
||||
# durable audit recording on every flex-auth decision that involves it.
|
||||
#
|
||||
# Reference: NetKingdom IAM Profile v0.1 §"Human Override and Emergency
|
||||
# Access". flex-auth maps this to principal_type=emergency and emits a
|
||||
# `record_emergency` obligation on every decision.
|
||||
|
||||
iss: https://sso.netkingdom.example/realms/netkingdom
|
||||
sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e
|
||||
aud:
|
||||
- flex-auth
|
||||
exp: 1767226200 # iat + 10 minutes; emergency tokens are short-lived
|
||||
iat: 1767225600
|
||||
auth_time: 1767225595
|
||||
azp: ops-console
|
||||
preferred_username: ada
|
||||
email: ada@netkingdom.example
|
||||
scope: openid profile hub:admin
|
||||
roles:
|
||||
- emergency
|
||||
- admin
|
||||
amr:
|
||||
- pwd
|
||||
- otp
|
||||
- hwk
|
||||
acr: "3"
|
||||
emergency:
|
||||
incident_id: INC-2026-0042
|
||||
authorized_by: "team:platform-stewards"
|
||||
reason: "credential rotation playbook step 4"
|
||||
24
examples/claims/key-cape-lightweight.yaml
Normal file
24
examples/claims/key-cape-lightweight.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# Claim envelope a key-cape (lightweight mode) deployment emits for an
|
||||
# authenticated human user. Profile-conformant minimum: required claims
|
||||
# only, single audience, simple roles list, OIDC standard amr values.
|
||||
#
|
||||
# Reference: docs/iam-profile-consumption.md, NetKingdom IAM Profile v0.1
|
||||
# §"Required Claims" and §"Local Development Profile".
|
||||
|
||||
iss: https://idp.netkingdom.local/keycape
|
||||
sub: user-7f9e2b
|
||||
aud:
|
||||
- flex-auth
|
||||
exp: 4102444800 # 2100-01-01, kept far-future for stable fixtures
|
||||
iat: 1767225600 # 2026-01-01
|
||||
preferred_username: ada
|
||||
email: ada@netkingdom.local
|
||||
name: Ada Lovelace
|
||||
scope: openid profile hub:read
|
||||
roles:
|
||||
- viewer
|
||||
amr:
|
||||
- pwd
|
||||
acr: "1"
|
||||
groups:
|
||||
- /markitect/readers
|
||||
26
examples/claims/keycloak-group-overage.yaml
Normal file
26
examples/claims/keycloak-group-overage.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Claim envelope when the token-side `groups` list has been clipped by
|
||||
# the IdP. Both Microsoft Entra and Keycloak signal this differently;
|
||||
# this fixture shows the Entra-style `hasgroups: true` flag. flex-auth
|
||||
# sets directory.group_overage = true and depends on the directory
|
||||
# resolver (FLEX-WP-0004 T05) to fetch the full set.
|
||||
#
|
||||
# Reference: docs/iam-profile-consumption.md §"Group Overage and
|
||||
# Freshness".
|
||||
|
||||
iss: https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0
|
||||
sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e
|
||||
aud:
|
||||
- flex-auth
|
||||
exp: 4102444800
|
||||
iat: 1767225600
|
||||
preferred_username: ada
|
||||
name: Ada Lovelace
|
||||
scope: openid profile hub:read
|
||||
roles:
|
||||
- viewer
|
||||
hasgroups: true
|
||||
_claim_names:
|
||||
groups: src1
|
||||
_claim_sources:
|
||||
src1:
|
||||
endpoint: https://graph.microsoft.com/v1.0/users/f1c4f64e/getMemberObjects
|
||||
43
examples/claims/keycloak-heavy.yaml
Normal file
43
examples/claims/keycloak-heavy.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
# Claim envelope a Keycloak (heavy mode) deployment emits for an
|
||||
# authenticated human user with MFA. Demonstrates the full set of
|
||||
# variations flex-auth must normalize: roles in realm_access AND
|
||||
# resource_access, scope as space-separated string, multiple audiences,
|
||||
# enriched assurance via amr=otp.
|
||||
#
|
||||
# Reference: docs/iam-profile-consumption.md §"Tolerated Variations".
|
||||
|
||||
iss: https://sso.netkingdom.example/realms/netkingdom
|
||||
sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e
|
||||
aud:
|
||||
- flex-auth
|
||||
- markitect-tool
|
||||
exp: 4102444800
|
||||
iat: 1767225600
|
||||
auth_time: 1767225590
|
||||
azp: markitect-cli
|
||||
preferred_username: ada
|
||||
email: ada@netkingdom.example
|
||||
email_verified: true
|
||||
name: Ada Lovelace
|
||||
given_name: Ada
|
||||
family_name: Lovelace
|
||||
scope: openid profile email hub:read hub:write hub:capability
|
||||
realm_access:
|
||||
roles:
|
||||
- default-roles-netkingdom
|
||||
- operator
|
||||
resource_access:
|
||||
flex-auth:
|
||||
roles:
|
||||
- reader
|
||||
markitect-tool:
|
||||
roles:
|
||||
- editor
|
||||
groups:
|
||||
- /platform/architecture
|
||||
- /markitect/readers
|
||||
amr:
|
||||
- pwd
|
||||
- otp
|
||||
acr: "2"
|
||||
sid: 4c0a3a8a-3a47-4f2f-8e89-9e5f9b0a0a0a
|
||||
20
examples/claims/service-account.yaml
Normal file
20
examples/claims/service-account.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# Claim envelope for a hub-to-hub service account (client_credentials
|
||||
# grant). Profile-required `service` role, scoped tightly to the
|
||||
# operation it performs. No preferred_username (service identities are
|
||||
# named after the service and environment per the profile).
|
||||
#
|
||||
# Reference: NetKingdom IAM Profile v0.1 §"Service Account Flow" and
|
||||
# §"Hub-to-Hub Service Account Pattern".
|
||||
|
||||
iss: https://sso.netkingdom.example/realms/netkingdom
|
||||
sub: svc-markitect-tool-prod
|
||||
aud:
|
||||
- flex-auth
|
||||
exp: 4102444800
|
||||
iat: 1767225600
|
||||
azp: svc-markitect-tool-prod
|
||||
client_id: svc-markitect-tool-prod
|
||||
scope: hub:read hub:capability
|
||||
roles:
|
||||
- service
|
||||
- operator
|
||||
99
examples/topaz/README.md
Normal file
99
examples/topaz/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Topaz alignment example
|
||||
|
||||
Runnable validation for the alignment commitment in ADR-003 and the
|
||||
mapping recorded in `docs/topaz-mapping-spike.md`. Boots Topaz, seeds
|
||||
a directory shaped like flex-auth's canonical vocabulary, and probes
|
||||
three permission scenarios.
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
cd examples/topaz
|
||||
docker compose up --abort-on-container-exit --exit-code-from probe
|
||||
```
|
||||
|
||||
Expected outcome (exit code 0):
|
||||
|
||||
```
|
||||
probe-1 | probe: steward-allow OK (check=true)
|
||||
probe-1 | probe: reader-allow OK (check=true)
|
||||
probe-1 | probe: outsider-deny OK (check=false)
|
||||
probe-1 | probe: all checks passed
|
||||
```
|
||||
|
||||
Tear down:
|
||||
|
||||
```sh
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## What the example proves
|
||||
|
||||
- Topaz's v3 manifest can express flex-auth's canonical object types
|
||||
(`user`, `group`, `tenant`, `knowledge_base`, `document`) and
|
||||
relations (`member`, `parent`, `owner_team`, `reader`, `steward`).
|
||||
- The Markitect fixture data
|
||||
(`examples/markitect/resource_manifest.yaml`, mirrored here) seeds
|
||||
the directory without translation.
|
||||
- Group→reader edges (`reader:platform-architecture` group with a
|
||||
`member` relation, plus a `reader` relation from the document to
|
||||
that group with `subject_relation=member`) resolve correctly via
|
||||
the manifest's `reader | group#member` union.
|
||||
- The `check` decision is fully derivable from directory data for the
|
||||
read-path case; no Rego is involved.
|
||||
|
||||
## File map
|
||||
|
||||
```
|
||||
manifest.yaml # Topaz v3 directory manifest
|
||||
policy/markitect.documents.rego # Rego module showing flex-auth's
|
||||
# canonical input shape (used by the
|
||||
# standalone evaluator; FLEX-WP-0004
|
||||
# T01 will bridge to Topaz's input)
|
||||
bundle/ # OPA bundle loaded into Topaz
|
||||
bundle/.manifest # OPA bundle root manifest
|
||||
bundle/policy/markitect.documents.rego # same Rego, mounted into Topaz
|
||||
data/objects.json # seed objects
|
||||
data/relations.json # seed relations
|
||||
cfg/config.yaml # Topaz config
|
||||
scripts/seed.sh # writes manifest + objects + relations
|
||||
scripts/probe.sh # three directory checks via REST
|
||||
docker-compose.yml # topaz, seed (one-shot), probe (one-shot)
|
||||
```
|
||||
|
||||
## Ports
|
||||
|
||||
When running, Topaz exposes (on `127.0.0.1` only):
|
||||
|
||||
| Port | Service |
|
||||
| --- | --- |
|
||||
| 8282 | authorizer gRPC |
|
||||
| 8383 | authorizer REST |
|
||||
| 9292 | directory gRPC (reader, writer, model, exporter, importer) |
|
||||
| 9393 | directory REST gateway |
|
||||
| 9494 | health |
|
||||
| 9696 | metrics |
|
||||
|
||||
Plaintext HTTP on the gateways. Internal gRPC uses TLS with
|
||||
auto-generated self-signed certs in a `topaz-certs` named volume; the
|
||||
`remote_directory.insecure: true` flag tells the in-process clients to
|
||||
accept them.
|
||||
|
||||
## Caveats
|
||||
|
||||
- Plaintext gateways are for the spike only. Real deployments use
|
||||
certs everywhere; see `docs/topaz-mapping-spike.md` §"Wire-Protocol
|
||||
Candidates" for the production posture.
|
||||
- The probe deliberately uses the directory `check` API instead of the
|
||||
authorizer `is` API. Bridging flex-auth's Rego input shape into
|
||||
Topaz's raw authorizer input is the Topaz adapter's job
|
||||
(FLEX-WP-0004 T01) and is intentionally out of scope for this
|
||||
validation. See `docs/topaz-mapping-spike.md` §"Implementation Notes
|
||||
Surfaced By The Spike".
|
||||
|
||||
## Pinned Topaz version
|
||||
|
||||
`ghcr.io/aserto-dev/topaz:latest` as resolved on 2026-05-16
|
||||
(digest `sha256:11fa7e2075870f3fe523afaadd942a6559b612f44b6bdb1296fe65299f5831fa`).
|
||||
FLEX-WP-0004 T01 will pin a specific tagged version once the adapter
|
||||
lands.
|
||||
1
examples/topaz/bundle/.manifest
Normal file
1
examples/topaz/bundle/.manifest
Normal file
@@ -0,0 +1 @@
|
||||
{"roots":["flexauth/markitect/documents"]}
|
||||
64
examples/topaz/bundle/policy/markitect.documents.rego
Normal file
64
examples/topaz/bundle/policy/markitect.documents.rego
Normal file
@@ -0,0 +1,64 @@
|
||||
package flexauth.markitect.documents
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
# This module is the Rego extracted from a flex-auth Rego-in-Markdown
|
||||
# policy package (ADR-002). Identical bytes ship to the standalone
|
||||
# evaluator and to Topaz; only the resolution of ds.* differs.
|
||||
#
|
||||
# Decision shape per ADR-002:
|
||||
# decision := {"effect": "...", "reason": "...", "obligations": [...]}
|
||||
# flex-auth wraps this into the canonical decision envelope.
|
||||
|
||||
default decision := {"effect": "deny", "reason": "no_matching_rule"}
|
||||
|
||||
# Reader on the document (direct or via group, or inherited from the
|
||||
# parent knowledge_base) is allowed to read/query/search.
|
||||
decision := {"effect": "allow", "reason": "reader_relation"} if {
|
||||
input.action in {"read", "query", "search"}
|
||||
input.resource.type == "document"
|
||||
is_reader
|
||||
}
|
||||
|
||||
# A steward on the document or parent may always read and may also
|
||||
# export (which carries an audit-export obligation).
|
||||
decision := {"effect": "allow", "reason": "steward_role"} if {
|
||||
input.action in {"read", "query", "search"}
|
||||
input.resource.type == "document"
|
||||
is_steward
|
||||
}
|
||||
|
||||
decision := {
|
||||
"effect": "allow",
|
||||
"reason": "steward_export",
|
||||
"obligations": [{"type": "record_export_receipt"}],
|
||||
} if {
|
||||
input.action == "export"
|
||||
input.resource.type == "document"
|
||||
is_steward
|
||||
}
|
||||
|
||||
# Helpers — these consult the directory shim (standalone) or Topaz's
|
||||
# ds.* builtins (delegated). The standalone evaluator registers
|
||||
# ds.check_relation / ds.check_permission with identical signatures.
|
||||
|
||||
is_reader if {
|
||||
ds.check_relation({
|
||||
"object_type": "document",
|
||||
"object_id": input.resource.id,
|
||||
"relation": "reader",
|
||||
"subject_type": "user",
|
||||
"subject_id": input.subject.id,
|
||||
})
|
||||
}
|
||||
|
||||
is_steward if {
|
||||
ds.check_relation({
|
||||
"object_type": "document",
|
||||
"object_id": input.resource.id,
|
||||
"relation": "steward",
|
||||
"subject_type": "user",
|
||||
"subject_id": input.subject.id,
|
||||
})
|
||||
}
|
||||
107
examples/topaz/cfg/config.yaml
Normal file
107
examples/topaz/cfg/config.yaml
Normal file
@@ -0,0 +1,107 @@
|
||||
# Topaz config for the flex-auth alignment spike.
|
||||
# Plaintext HTTP gateways for local convenience — never use this shape
|
||||
# in production. See docs/topaz-mapping-spike.md.
|
||||
|
||||
version: 2
|
||||
|
||||
logging:
|
||||
prod: false
|
||||
log_level: info
|
||||
|
||||
directory:
|
||||
db_path: /db/directory.db
|
||||
request_timeout: 5s
|
||||
seed_metadata: false
|
||||
|
||||
remote_directory:
|
||||
address: "0.0.0.0:9292"
|
||||
insecure: true
|
||||
|
||||
jwt:
|
||||
acceptable_time_skew_seconds: 5
|
||||
|
||||
api:
|
||||
health:
|
||||
listen_address: "0.0.0.0:9494"
|
||||
metrics:
|
||||
listen_address: "0.0.0.0:9696"
|
||||
services:
|
||||
reader:
|
||||
grpc:
|
||||
listen_address: "0.0.0.0:9292"
|
||||
certs:
|
||||
tls_key_path: "/certs/grpc.key"
|
||||
tls_cert_path: "/certs/grpc.crt"
|
||||
tls_ca_cert_path: "/certs/grpc-ca.crt"
|
||||
gateway:
|
||||
listen_address: "0.0.0.0:9393"
|
||||
allowed_origins:
|
||||
- "*"
|
||||
http: true
|
||||
read_timeout: 2s
|
||||
write_timeout: 2s
|
||||
idle_timeout: 30s
|
||||
writer:
|
||||
needs: [reader]
|
||||
grpc:
|
||||
listen_address: "0.0.0.0:9292"
|
||||
certs:
|
||||
tls_key_path: "/certs/grpc.key"
|
||||
tls_cert_path: "/certs/grpc.crt"
|
||||
tls_ca_cert_path: "/certs/grpc-ca.crt"
|
||||
gateway:
|
||||
listen_address: "0.0.0.0:9393"
|
||||
allowed_origins: ["*"]
|
||||
http: true
|
||||
model:
|
||||
needs: [reader]
|
||||
grpc:
|
||||
listen_address: "0.0.0.0:9292"
|
||||
certs:
|
||||
tls_key_path: "/certs/grpc.key"
|
||||
tls_cert_path: "/certs/grpc.crt"
|
||||
tls_ca_cert_path: "/certs/grpc-ca.crt"
|
||||
gateway:
|
||||
listen_address: "0.0.0.0:9393"
|
||||
allowed_origins: ["*"]
|
||||
http: true
|
||||
exporter:
|
||||
needs: [reader]
|
||||
grpc:
|
||||
listen_address: "0.0.0.0:9292"
|
||||
certs:
|
||||
tls_key_path: "/certs/grpc.key"
|
||||
tls_cert_path: "/certs/grpc.crt"
|
||||
tls_ca_cert_path: "/certs/grpc-ca.crt"
|
||||
importer:
|
||||
needs: [reader]
|
||||
grpc:
|
||||
listen_address: "0.0.0.0:9292"
|
||||
certs:
|
||||
tls_key_path: "/certs/grpc.key"
|
||||
tls_cert_path: "/certs/grpc.crt"
|
||||
tls_ca_cert_path: "/certs/grpc-ca.crt"
|
||||
authorizer:
|
||||
needs: [reader]
|
||||
grpc:
|
||||
connection_timeout_seconds: 2
|
||||
listen_address: "0.0.0.0:8282"
|
||||
certs:
|
||||
tls_key_path: "/certs/grpc.key"
|
||||
tls_cert_path: "/certs/grpc.crt"
|
||||
tls_ca_cert_path: "/certs/grpc-ca.crt"
|
||||
gateway:
|
||||
listen_address: "0.0.0.0:8383"
|
||||
allowed_origins: ["*"]
|
||||
http: true
|
||||
read_timeout: 2s
|
||||
write_timeout: 2s
|
||||
idle_timeout: 30s
|
||||
|
||||
opa:
|
||||
instance_id: "flex-auth-spike"
|
||||
graceful_shutdown_period_seconds: 2
|
||||
local_bundles:
|
||||
paths:
|
||||
- "/bundle"
|
||||
skip_verification: true
|
||||
20
examples/topaz/data/objects.json
Normal file
20
examples/topaz/data/objects.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"objects": [
|
||||
{"type": "tenant", "id": "platform"},
|
||||
{"type": "group", "id": "team:platform-architecture", "display_name": "Platform Architecture"},
|
||||
{"type": "group", "id": "reader:platform-architecture", "display_name": "Platform Architecture Readers"},
|
||||
{"type": "user", "id": "alice@example.test", "display_name": "Alice (steward)"},
|
||||
{"type": "user", "id": "bob@example.test", "display_name": "Bob (reader)"},
|
||||
{"type": "user", "id": "eve@example.test", "display_name": "Eve (outsider)"},
|
||||
{
|
||||
"type": "knowledge_base",
|
||||
"id": "knowledge-base:markitect-example",
|
||||
"properties": {"trust_zone": "public", "labels": ["public"]}
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"id": "document:internal-note",
|
||||
"properties": {"trust_zone": "internal", "labels": ["internal"], "path": "examples/policy/private/internal-note.md"}
|
||||
}
|
||||
]
|
||||
}
|
||||
10
examples/topaz/data/relations.json
Normal file
10
examples/topaz/data/relations.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"relations": [
|
||||
{"object_type": "group", "object_id": "team:platform-architecture", "relation": "member", "subject_type": "user", "subject_id": "alice@example.test"},
|
||||
{"object_type": "group", "object_id": "reader:platform-architecture", "relation": "member", "subject_type": "user", "subject_id": "bob@example.test"},
|
||||
{"object_type": "knowledge_base", "object_id": "knowledge-base:markitect-example", "relation": "owner_team", "subject_type": "group", "subject_id": "team:platform-architecture"},
|
||||
{"object_type": "document", "object_id": "document:internal-note", "relation": "parent", "subject_type": "knowledge_base", "subject_id": "knowledge-base:markitect-example"},
|
||||
{"object_type": "document", "object_id": "document:internal-note", "relation": "steward", "subject_type": "user", "subject_id": "alice@example.test"},
|
||||
{"object_type": "document", "object_id": "document:internal-note", "relation": "reader", "subject_type": "group", "subject_id": "reader:platform-architecture", "subject_relation": "member"}
|
||||
]
|
||||
}
|
||||
68
examples/topaz/docker-compose.yml
Normal file
68
examples/topaz/docker-compose.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
# Runnable Topaz example for the flex-auth alignment spike.
|
||||
#
|
||||
# Boot order:
|
||||
# 1. topaz — runs topazd with the spike config; serves authorizer
|
||||
# on :8282 (gRPC) and :8383 (REST), directory on :9292
|
||||
# (gRPC) and :9393 (REST), health on :9494.
|
||||
# 2. seed — one-shot container that pushes the manifest and seeds
|
||||
# directory objects/relations via REST. Exits on success.
|
||||
# 3. probe — one-shot container that runs three authorizer checks
|
||||
# (steward allow, reader allow, outsider deny) and exits
|
||||
# non-zero if any decision is unexpected.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose up --abort-on-container-exit --exit-code-from probe
|
||||
#
|
||||
# See docs/topaz-mapping-spike.md and README.md.
|
||||
|
||||
services:
|
||||
topaz:
|
||||
image: ghcr.io/aserto-dev/topaz:latest
|
||||
command: ["run", "--config-file", "/cfg/config.yaml", "--bundle", "/bundle"]
|
||||
ports:
|
||||
- "127.0.0.1:8282:8282" # authorizer gRPC
|
||||
- "127.0.0.1:8383:8383" # authorizer REST
|
||||
- "127.0.0.1:9292:9292" # directory gRPC
|
||||
- "127.0.0.1:9393:9393" # directory REST
|
||||
- "127.0.0.1:9494:9494" # health
|
||||
volumes:
|
||||
- ./cfg:/cfg:ro
|
||||
- ./bundle:/bundle:ro
|
||||
- topaz-db:/db
|
||||
- topaz-certs:/certs
|
||||
healthcheck:
|
||||
# Topaz's image has no curl/wget; nc is in busybox. Probe TCP on
|
||||
# the authorizer REST port — the gateway only listens once the
|
||||
# backing gRPC service is ready.
|
||||
test: ["CMD-SHELL", "nc -z 127.0.0.1 8383 || exit 1"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 30
|
||||
|
||||
seed:
|
||||
image: alpine:3.20
|
||||
depends_on:
|
||||
topaz:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./data:/data:ro
|
||||
- ./scripts:/scripts:ro
|
||||
- ./manifest.yaml:/manifest.yaml:ro
|
||||
entrypoint: ["/bin/sh", "/scripts/seed.sh"]
|
||||
environment:
|
||||
DIRECTORY_REST: "http://topaz:9393"
|
||||
|
||||
probe:
|
||||
image: alpine:3.20
|
||||
depends_on:
|
||||
seed:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- ./scripts:/scripts:ro
|
||||
entrypoint: ["/bin/sh", "/scripts/probe.sh"]
|
||||
environment:
|
||||
AUTHORIZER_REST: "http://topaz:8383"
|
||||
|
||||
volumes:
|
||||
topaz-db:
|
||||
topaz-certs:
|
||||
49
examples/topaz/manifest.yaml
Normal file
49
examples/topaz/manifest.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
# Topaz v3 manifest for the flex-auth alignment spike.
|
||||
#
|
||||
# Mirrors flex-auth's canonical resource/subject/group/relation
|
||||
# vocabulary, scoped to the subset the Markitect internal-document
|
||||
# fixture exercises. Reference: docs/topaz-mapping-spike.md.
|
||||
#
|
||||
# Notes on Topaz syntax:
|
||||
# - relations: union types only ( | ) and group-member shorthand ( # ).
|
||||
# - permissions: also support the parent-walk operator ( -> ).
|
||||
# yaml-language-server: $schema=https://www.topaz.sh/schema/manifest.json
|
||||
---
|
||||
model:
|
||||
version: 3
|
||||
|
||||
types:
|
||||
user:
|
||||
relations:
|
||||
manager: user
|
||||
|
||||
group:
|
||||
relations:
|
||||
member: user | group#member
|
||||
|
||||
tenant:
|
||||
relations:
|
||||
member: user | group#member
|
||||
|
||||
knowledge_base:
|
||||
relations:
|
||||
tenant: tenant
|
||||
owner_team: group
|
||||
reader: user | group#member
|
||||
steward: user | group#member
|
||||
permissions:
|
||||
read: reader | steward
|
||||
admin: steward
|
||||
|
||||
document:
|
||||
relations:
|
||||
parent: knowledge_base
|
||||
owner_team: group
|
||||
reader: user | group#member
|
||||
steward: user | group#member
|
||||
permissions:
|
||||
read: reader | steward | parent->read
|
||||
query: reader | steward | parent->read
|
||||
search: reader | steward | parent->read
|
||||
export: steward
|
||||
admin: steward | parent->admin
|
||||
64
examples/topaz/policy/markitect.documents.rego
Normal file
64
examples/topaz/policy/markitect.documents.rego
Normal file
@@ -0,0 +1,64 @@
|
||||
package flexauth.markitect.documents
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
# This module is the Rego extracted from a flex-auth Rego-in-Markdown
|
||||
# policy package (ADR-002). Identical bytes ship to the standalone
|
||||
# evaluator and to Topaz; only the resolution of ds.* differs.
|
||||
#
|
||||
# Decision shape per ADR-002:
|
||||
# decision := {"effect": "...", "reason": "...", "obligations": [...]}
|
||||
# flex-auth wraps this into the canonical decision envelope.
|
||||
|
||||
default decision := {"effect": "deny", "reason": "no_matching_rule"}
|
||||
|
||||
# Reader on the document (direct or via group, or inherited from the
|
||||
# parent knowledge_base) is allowed to read/query/search.
|
||||
decision := {"effect": "allow", "reason": "reader_relation"} if {
|
||||
input.action in {"read", "query", "search"}
|
||||
input.resource.type == "document"
|
||||
is_reader
|
||||
}
|
||||
|
||||
# A steward on the document or parent may always read and may also
|
||||
# export (which carries an audit-export obligation).
|
||||
decision := {"effect": "allow", "reason": "steward_role"} if {
|
||||
input.action in {"read", "query", "search"}
|
||||
input.resource.type == "document"
|
||||
is_steward
|
||||
}
|
||||
|
||||
decision := {
|
||||
"effect": "allow",
|
||||
"reason": "steward_export",
|
||||
"obligations": [{"type": "record_export_receipt"}],
|
||||
} if {
|
||||
input.action == "export"
|
||||
input.resource.type == "document"
|
||||
is_steward
|
||||
}
|
||||
|
||||
# Helpers — these consult the directory shim (standalone) or Topaz's
|
||||
# ds.* builtins (delegated). The standalone evaluator registers
|
||||
# ds.check_relation / ds.check_permission with identical signatures.
|
||||
|
||||
is_reader if {
|
||||
ds.check_relation({
|
||||
"object_type": "document",
|
||||
"object_id": input.resource.id,
|
||||
"relation": "reader",
|
||||
"subject_type": "user",
|
||||
"subject_id": input.subject.id,
|
||||
})
|
||||
}
|
||||
|
||||
is_steward if {
|
||||
ds.check_relation({
|
||||
"object_type": "document",
|
||||
"object_id": input.resource.id,
|
||||
"relation": "steward",
|
||||
"subject_type": "user",
|
||||
"subject_id": input.subject.id,
|
||||
})
|
||||
}
|
||||
66
examples/topaz/scripts/probe.sh
Executable file
66
examples/topaz/scripts/probe.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/sh
|
||||
# Probe the Topaz directory's Check API to verify the seeded manifest
|
||||
# correctly resolves reader/steward/outsider permissions for the
|
||||
# Markitect internal-document fixture. Exits 0 if all checks match
|
||||
# expectations.
|
||||
#
|
||||
# This probe deliberately uses the directory Check API rather than the
|
||||
# authorizer Is API. The manifest permissions are the substrate the
|
||||
# Topaz adapter (FLEX-WP-0004 T01) and the standalone evaluator both
|
||||
# consult; demonstrating it works end-to-end here is the spike's actual
|
||||
# validation question. Bridging flex-auth's Rego input shape into
|
||||
# Topaz's raw authorizer input is adapter work, intentionally out of
|
||||
# this spike's scope (see docs/topaz-mapping-spike.md §"Implementation
|
||||
# Notes").
|
||||
|
||||
set -eu
|
||||
|
||||
apk add --no-cache curl jq >/dev/null
|
||||
|
||||
DIR="${DIRECTORY_REST:-http://topaz:9393}"
|
||||
echo "probe: directory REST = $DIR"
|
||||
|
||||
check() {
|
||||
name="$1"
|
||||
subject="$2"
|
||||
resource="$3"
|
||||
permission="$4"
|
||||
expect="$5" # "true" or "false"
|
||||
|
||||
body=$(cat <<EOF
|
||||
{
|
||||
"object_type": "document",
|
||||
"object_id": "$resource",
|
||||
"relation": "$permission",
|
||||
"subject_type": "user",
|
||||
"subject_id": "$subject"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
response=$(curl -sf -X POST "$DIR/api/v3/directory/check" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$body")
|
||||
|
||||
echo "probe: $name => $response"
|
||||
|
||||
got=$(echo "$response" | jq -r '.check')
|
||||
if [ "$got" = "$expect" ]; then
|
||||
echo "probe: $name OK (check=$got)"
|
||||
else
|
||||
echo "probe: $name FAIL (check=$got; expected=$expect)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Three scenarios on the seeded directory:
|
||||
# 1. Alice is a steward on the document, so read should be permitted.
|
||||
# 2. Bob is a member of reader:platform-architecture, which is the
|
||||
# reader on the document via subject_relation=member, so read should
|
||||
# be permitted via the reader|group#member union in the manifest.
|
||||
# 3. Eve has no relation to the document, so read should be denied.
|
||||
check "steward-allow" "alice@example.test" "document:internal-note" "read" "true"
|
||||
check "reader-allow" "bob@example.test" "document:internal-note" "read" "true"
|
||||
check "outsider-deny" "eve@example.test" "document:internal-note" "read" "false"
|
||||
|
||||
echo "probe: all checks passed"
|
||||
43
examples/topaz/scripts/seed.sh
Executable file
43
examples/topaz/scripts/seed.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
# Seed the Topaz directory: push the manifest, then objects and relations.
|
||||
# Uses Topaz's directory REST gateway. Exits 0 on success.
|
||||
|
||||
set -eu
|
||||
|
||||
apk add --no-cache curl jq >/dev/null
|
||||
|
||||
DIR="${DIRECTORY_REST:-http://topaz:9393}"
|
||||
echo "seed: directory REST = $DIR"
|
||||
|
||||
# 1. Push the directory model (manifest).
|
||||
echo "seed: setting model"
|
||||
curl -sf -X POST "$DIR/api/v3/directory/manifest" \
|
||||
-H 'Content-Type: application/yaml' \
|
||||
--data-binary @/manifest.yaml \
|
||||
|| curl -sf -X POST "$DIR/api/v3/model" \
|
||||
-H 'Content-Type: application/yaml' \
|
||||
--data-binary @/manifest.yaml
|
||||
|
||||
echo
|
||||
|
||||
# 2. Push objects.
|
||||
echo "seed: writing objects"
|
||||
jq -c '.objects[]' /data/objects.json | while IFS= read -r obj; do
|
||||
curl -sf -X POST "$DIR/api/v3/directory/object" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"object\":$obj}" >/dev/null
|
||||
printf '.'
|
||||
done
|
||||
echo
|
||||
|
||||
# 3. Push relations.
|
||||
echo "seed: writing relations"
|
||||
jq -c '.relations[]' /data/relations.json | while IFS= read -r rel; do
|
||||
curl -sf -X POST "$DIR/api/v3/directory/relation" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"relation\":$rel}" >/dev/null
|
||||
printf '.'
|
||||
done
|
||||
echo
|
||||
|
||||
echo "seed: done"
|
||||
@@ -3,7 +3,7 @@ id: FLEX-WP-0005
|
||||
type: workplan
|
||||
title: "Foundations and Topaz Alignment"
|
||||
domain: netkingdom
|
||||
status: todo
|
||||
status: done
|
||||
owner: flex-auth
|
||||
topic_slug: flex-auth
|
||||
planning_priority: P0
|
||||
@@ -125,7 +125,7 @@ flex-auth as the canonical owner of the manifest shape.
|
||||
|
||||
```task
|
||||
id: FLEX-WP-0005-T004
|
||||
status: in_progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b8a314c3-e98e-4093-bb11-ab8546b8d79b"
|
||||
```
|
||||
@@ -154,7 +154,7 @@ both `FLEX-WP-0002 P2.1` and `FLEX-WP-0004 T001`.
|
||||
|
||||
```task
|
||||
id: FLEX-WP-0005-T005
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "b31dab7b-e72c-4abe-b6d5-f5875fd0c25a"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user