Files
flex-auth/docs/topaz-mapping-spike.md
tegwick 82177d88a9 Topaz alignment spike — mapping doc + green e2e example
Closes FLEX-WP-0005 T04. Validates ADR-003's commitment to shape the
standalone core for cheap Topaz adapter work.

Spike output:
- docs/topaz-mapping-spike.md — vocabulary map (subject, group, tenant,
  knowledge_base, document, plus parent / owner_team / reader / steward /
  member relations), Rego module shape, decision envelope, wire-protocol
  ranking (gRPC primary, REST fallback, embedding rejected), schema
  restatement recommendation, implications for FLEX-WP-0002 / 0004.
- examples/topaz/ — runnable docker-compose deploying Topaz with the
  flex-auth-shaped manifest. seed and probe one-shots cover three
  scenarios: alice (steward) allow, bob (group→reader) allow, eve
  (outsider) deny. End-to-end green on 2026-05-16:

    probe: steward-allow OK (check=true)
    probe: reader-allow  OK (check=true)
    probe: outsider-deny OK (check=false)
    probe: all checks passed

Key findings recorded as Implementation Notes in the spike doc:
- Rego input contract bridging (Topaz raw shape ↔ flex-auth canonical
  shape) is adapter scope, not core scope.
- Topaz identity objects are a Topaz convention; the adapter
  materializes them at directory import time.
- Directory-only permission resolution is sufficient for the common
  case; Rego is reserved for context-dependent decisions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:04:42 +02:00

15 KiB

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:

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:

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 librarynot 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)

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 APICheck 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.