generated from coulomb/repo-seed
Compare commits
71 Commits
9aab86b204
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b4520bd731 | |||
| 941501c590 | |||
| fae0f00a69 | |||
| 77bcd55ddb | |||
| f0d1afa237 | |||
| 0fde95a87c | |||
| 53e0d055c9 | |||
| e1c141234a | |||
| 8a913d6163 | |||
| 1be449dae8 | |||
| 1b899cd41c | |||
| 2230163de1 | |||
| 3247f5d357 | |||
| aa8e3a4e34 | |||
| 8354485632 | |||
| 12c4bed6f4 | |||
| af3e8b2af2 | |||
| 99a521e176 | |||
| 1f5e9626e5 | |||
| 32933c71f9 | |||
| ac9cf09545 | |||
| 360025e38b | |||
| 3fdbc7acb7 | |||
| ad4895187b | |||
| 4bb329c921 | |||
| 90021d16b6 | |||
| 8a61e40bd6 | |||
| 1ce0181e8f | |||
| 0fbb2a45c2 | |||
| 184ce5a380 | |||
| 131fd2cd9b | |||
| 3d1967cb41 | |||
| 7e09a21c5f | |||
| 96e53bf1d9 | |||
| 1c915f12d7 | |||
| b6712850c3 | |||
| 50e436093a | |||
| 9e2591c1f4 | |||
| dd4f688ab6 | |||
| 12dbf52586 | |||
| a285959183 | |||
| 6586adb4f5 | |||
| 4c9f964425 | |||
| 6bff4cd7c9 | |||
| 18054bd160 | |||
| 49655e40e0 | |||
| 61e113f8b6 | |||
| ccf68332f8 | |||
| 2b103ea70b | |||
| 4342f98d83 | |||
| faea068721 | |||
| aa70dbebe1 | |||
| 54984585e3 | |||
| fa1b42e678 | |||
| 550d096cb2 | |||
| 2cce434d47 | |||
| 3c4f8fc2b4 | |||
| 4f4c290684 | |||
| 7fdf6d63d5 | |||
| dd0b9663c4 | |||
| f930e96568 | |||
| 7471e07cbb | |||
| 82177d88a9 | |||
| 52b5575048 | |||
| f41fa1abb7 | |||
| f885e6d762 | |||
| e2d410de6e | |||
| 55120ec20a | |||
| 485b3992de | |||
| 36a3d3c898 | |||
| 15155c4c40 |
20
.claude/rules/agents.md
Normal file
20
.claude/rules/agents.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Kaizen Agents
|
||||
|
||||
Specialized agent personas available on demand via the state-hub MCP.
|
||||
|
||||
**Discover:** `list_kaizen_agents()` — returns all agents with name, description, category
|
||||
**Load:** `get_kaizen_agent("tdd-workflow")` — returns full instructions; read and follow them
|
||||
|
||||
Common agents:
|
||||
|
||||
| Agent | Category | When to use |
|
||||
|-------|----------|-------------|
|
||||
| `tdd-workflow` | testing | Step-by-step TDD8 workflow for any feature |
|
||||
| `code-refactoring` | quality | Code quality analysis and safe refactoring |
|
||||
| `test-maintenance` | testing | Diagnose and fix failing tests |
|
||||
| `requirements-engineering` | process | Prevent interface/mock mismatches upfront |
|
||||
| `keepaTodofile` | process | Maintain TODO.md during work |
|
||||
| `project-management` | process | Track status, determine next steps |
|
||||
| `datamodel-optimization` | quality | Optimize dataclasses and data structures |
|
||||
|
||||
All 17 agents: call `list_kaizen_agents()` for the full list.
|
||||
8
.claude/rules/architecture.md
Normal file
8
.claude/rules/architecture.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## Architecture
|
||||
|
||||
<!-- TODO: Describe the key design decisions and component structure.
|
||||
Key modules, data flows, external integrations, state machines, etc. -->
|
||||
|
||||
## Quick Reference
|
||||
|
||||
`~/state-hub/mcp_server/TOOLS.md` — MCP tool reference
|
||||
50
.claude/rules/credential-routing.md
Normal file
50
.claude/rules/credential-routing.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Credential and access routing
|
||||
|
||||
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
|
||||
for inference. Run this check **before** requesting secrets, API keys, SSH access,
|
||||
login tokens, or database passwords — in any repo, not only `ops-warden`.
|
||||
|
||||
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
|
||||
other credential need belongs to another subsystem. **Do not** message
|
||||
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
|
||||
|
||||
### Lookup (do this first)
|
||||
|
||||
```bash
|
||||
warden route find "<describe your need>" --json
|
||||
warden route show <catalog-id> --json
|
||||
```
|
||||
|
||||
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
|
||||
|
||||
| Agent runtime | How to orient |
|
||||
| --- | --- |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=flex-auth` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
|
||||
|
||||
### Quick routing table
|
||||
|
||||
| I need… | Owner | ops-warden executes? |
|
||||
| --- | --- | --- |
|
||||
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
|
||||
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
|
||||
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
|
||||
| Authorization decision | flex-auth | No — route only |
|
||||
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
|
||||
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
|
||||
|
||||
### Anti-patterns (do not do these)
|
||||
|
||||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
Non-credential capabilities are usually discovered through **reuse-surface** federation
|
||||
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
|
||||
every repo's agent instructions because it is high-frequency, high-risk, and easy to
|
||||
get wrong.
|
||||
|
||||
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
|
||||
38
.claude/rules/first-session.md
Normal file
38
.claude/rules/first-session.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## First Session Protocol
|
||||
|
||||
Triggered when `get_domain_summary("infotech")` shows **no workstreams**.
|
||||
The project is registered but work has not yet been structured.
|
||||
|
||||
**Step 1 — Read, don't write**
|
||||
- `~/the-custodian/canon/projects/infotech/project_charter_v0.1.md` — purpose, scope
|
||||
- `~/the-custodian/canon/projects/infotech/roadmap_v0.1.md` — planned phases
|
||||
- Scan repo root: README, directory structure, existing code or docs
|
||||
|
||||
**Step 2 — Survey in-progress work**
|
||||
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
|
||||
|
||||
**Step 3 — Propose workstreams to Bernd**
|
||||
Propose 1–3 workstreams — each a coherent strand, weeks to months, anchored to a
|
||||
roadmap phase. **Wait for approval before creating.**
|
||||
|
||||
**Step 4 — Create workplan file first, then DB record (ADR-001)**
|
||||
```
|
||||
workplans/FLEX-WP-NNNN-<slug>.md ← write this first
|
||||
```
|
||||
Then register in the hub:
|
||||
```
|
||||
create_workstream(topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", title="...", owner="...", description="...")
|
||||
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
|
||||
```
|
||||
|
||||
**Step 5 — Record the setup**
|
||||
```
|
||||
add_progress_event(
|
||||
summary="First session: structured infotech into N workstreams, M tasks",
|
||||
event_type="milestone",
|
||||
topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a",
|
||||
detail={"workstreams": [...], "tasks_created": M}
|
||||
)
|
||||
```
|
||||
|
||||
<!-- Delete or archive this file once past first session -->
|
||||
8
.claude/rules/repo-boundary.md
Normal file
8
.claude/rules/repo-boundary.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## Repo boundary
|
||||
|
||||
This repo owns **flex-auth** only. It does not own:
|
||||
|
||||
<!-- TODO: List what belongs in adjacent repos, e.g.:
|
||||
- SSH key management → railiance-infra/
|
||||
- State hub code → state-hub/
|
||||
-->
|
||||
5
.claude/rules/repo-identity.md
Normal file
5
.claude/rules/repo-identity.md
Normal file
@@ -0,0 +1,5 @@
|
||||
**Purpose:** flex-auth - (fill in purpose)
|
||||
|
||||
**Domain:** infotech
|
||||
**Repo slug:** flex-auth
|
||||
**Topic ID:** cee7bedf-2b48-46ef-8601-006474f2ad7a
|
||||
85
.claude/rules/session-protocol.md
Normal file
85
.claude/rules/session-protocol.md
Normal file
@@ -0,0 +1,85 @@
|
||||
## Session Protocol
|
||||
|
||||
Dev Hub (State Hub API): http://127.0.0.1:8000
|
||||
MCP server name in `~/.claude.json`: `dev-hub`
|
||||
|
||||
**Step 1 — Orient**
|
||||
|
||||
Read the offline-safe brief first — it works without a live hub connection:
|
||||
```bash
|
||||
cat .custodian-brief.md
|
||||
```
|
||||
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
|
||||
```
|
||||
get_domain_summary("infotech")
|
||||
```
|
||||
If MCP tools are unavailable in the current agent session, use the REST API:
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool
|
||||
```
|
||||
If the hub is offline: `cd ~/state-hub && make api`
|
||||
|
||||
**Step 2 — Check inbox**
|
||||
With MCP tools:
|
||||
```
|
||||
get_messages(to_agent="flex-auth", unread_only=True)
|
||||
```
|
||||
Mark read with `mark_message_read(message_id)`. Reply or act on coordination
|
||||
requests before proceeding.
|
||||
|
||||
Without MCP tools:
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/messages/?to_agent=flex-auth&unread_only=true" \
|
||||
| python3 -m json.tool
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
||||
-H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
|
||||
**Step 3 — Scan workplans**
|
||||
```bash
|
||||
ls workplans/
|
||||
```
|
||||
For each file with `status: ready`, `active`, or `blocked`, note pending
|
||||
`wait`/`todo`/`progress` tasks.
|
||||
|
||||
**Step 4 — Present brief**
|
||||
|
||||
1. **Active workstreams** for `infotech` — title, task counts, blocking decisions
|
||||
2. **Pending tasks** from `workplans/` + any `[repo:flex-auth]` hub tasks
|
||||
3. **Goal guidance** — if `goal_guidance` in summary:
|
||||
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
|
||||
- `alignment_warnings`: flag if active work is not aligned with current goal
|
||||
4. **Suggested next action** — highest-priority open item
|
||||
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
|
||||
|
||||
If no workstreams: follow First Session Protocol (`first-session.md`).
|
||||
|
||||
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
|
||||
|
||||
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
|
||||
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
|
||||
|
||||
**Session close:**
|
||||
With MCP tools:
|
||||
```
|
||||
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workstream_id="<uuid>")
|
||||
```
|
||||
Without MCP tools:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
|
||||
```
|
||||
If workplan files were modified, ensure the local copy is up to date first:
|
||||
```bash
|
||||
git -C <repo_path> pull --ff-only
|
||||
cd ~/state-hub && make fix-consistency REPO=flex-auth
|
||||
```
|
||||
For repos where implementation runs on a remote machine (e.g. CoulombCore),
|
||||
use the combined target which pulls before fixing:
|
||||
```bash
|
||||
cd ~/state-hub && make fix-consistency-remote REPO=flex-auth
|
||||
```
|
||||
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
|
||||
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
|
||||
until you pull — intentional to prevent clobbering remote progress.
|
||||
19
.claude/rules/stack-and-commands.md
Normal file
19
.claude/rules/stack-and-commands.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## Stack
|
||||
|
||||
<!-- TODO: Fill in language, frameworks, and key dependencies -->
|
||||
- **Language:**
|
||||
- **Key deps:**
|
||||
|
||||
## Dev Commands
|
||||
|
||||
```bash
|
||||
# TODO: Fill in the standard commands for this repo
|
||||
|
||||
# Install dependencies
|
||||
|
||||
# Run tests
|
||||
|
||||
# Lint / type check
|
||||
|
||||
# Build / package (if applicable)
|
||||
```
|
||||
40
.claude/rules/workplan-convention.md
Normal file
40
.claude/rules/workplan-convention.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Workplan Convention (ADR-001)
|
||||
|
||||
File location: `workplans/FLEX-WP-NNNN-<slug>.md`
|
||||
ID prefix: `FLEX-WP-`
|
||||
|
||||
Work items originate as files in this repo **before** being registered in the hub.
|
||||
|
||||
Canonical workplan/workstream frontmatter statuses are:
|
||||
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
|
||||
Use `proposed` for a newly drafted plan, `ready` after review against current
|
||||
repo state, and `finished` when implementation is complete. `stalled` and
|
||||
`needs_review` are derived health labels, not stored statuses.
|
||||
|
||||
Closed workplans may be moved to `workplans/archived/` with a completion-date
|
||||
prefix: `YYMMDD-FLEX-WP-NNNN-<slug>.md`. The frontmatter id remains
|
||||
unchanged; the prefix is only for quick visual reference.
|
||||
|
||||
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
|
||||
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
|
||||
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
|
||||
directly. Promote anything requiring analysis, design, approval, dependencies, or
|
||||
multiple planned phases into a normal workplan.
|
||||
|
||||
Ecosystem todos from other agents arrive as `[repo:flex-auth]` hub tasks —
|
||||
visible at session start. Pick one up by creating the workplan file, then registering
|
||||
the workstream.
|
||||
|
||||
Task blocks use this shape:
|
||||
|
||||
```task
|
||||
id: FLEX-WP-NNNN-T01
|
||||
status: wait | todo | progress | done | cancel
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
```
|
||||
|
||||
Status progression is `todo` → `progress` → `done`; use `wait` for waiting or
|
||||
blocked work and `cancel` for stopped work.
|
||||
|
||||
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->
|
||||
@@ -1,61 +1,23 @@
|
||||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# Custodian Brief — flex-auth
|
||||
|
||||
**Domain:** netkingdom
|
||||
**Last synced:** 2026-05-15 20:29 UTC
|
||||
**Domain:** infotech
|
||||
**Last synced:** 2026-06-24 11:11 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
||||
### Foundations and Topaz Alignment
|
||||
Progress: 1/6 done | workstream_id: `e37d42a9-0018-4a67-a672-ff4e9716b338`
|
||||
### Ops-Warden Policy Gate Production Deployment
|
||||
Progress: 4/5 done | workstream_id: `358ce697-2611-4fe9-89ab-63e86ceb00fa`
|
||||
|
||||
**Open tasks:**
|
||||
- · P5.2 - Land Go project skeleton `8ac73c33`
|
||||
- · P5.3 - Pin FlexAuthResourceManifest schema `80285e1e`
|
||||
- · P5.4 - Topaz alignment spike `b8a314c3`
|
||||
- · P5.5 - Cite NetKingdom IAM Profile and pin claim consumption `b31dab7b`
|
||||
- · P5.6 - Confirm ops-warden boundary `dcd45a14`
|
||||
|
||||
### Standalone Policy-as-Code Core
|
||||
Progress: 0/8 done | workstream_id: `aa60e183-9a87-4e03-99b0-15786bfa11ae`
|
||||
|
||||
**Open tasks:**
|
||||
- · P2.1 - Define canonical schemas `534e5251`
|
||||
- · P2.2 - Implement local registry store `d8045124`
|
||||
- · P2.3 - Implement policy package loader and validator `09be0f25`
|
||||
- · P2.4 - Implement deterministic check and batch_check APIs `f6427575`
|
||||
- · P2.5 - Implement list_allowed and explain `e8fcbabd`
|
||||
- · P2.6 - Add local decision log `2def10c1`
|
||||
- · P2.7 - Add CLI and service skeleton `ee9ae6dd`
|
||||
- … and 1 more open tasks
|
||||
|
||||
### Markitect Consumer Integration
|
||||
Progress: 0/6 done | workstream_id: `c0a6c9f6-bb6b-416d-b537-f30504c63d75`
|
||||
|
||||
**Open tasks:**
|
||||
- · P3.1 - Define Markitect resource namespace `53f2fa67`
|
||||
- · P3.2 - Import Markitect resource manifests `90082eaf`
|
||||
- · P3.3 - Define Markitect action vocabulary `cfc78bbb`
|
||||
- · P3.4 - Implement Markitect check fixtures `1d5de3b2`
|
||||
- · P3.5 - Add Markitect adapter contract tests `f9297b0d`
|
||||
- · P3.6 - Document integration flow `e34b0303`
|
||||
|
||||
### Delegated PDP and Directory Adapters
|
||||
Progress: 0/6 done | workstream_id: `99a82976-d376-42b0-89cc-c44e01c0bec6`
|
||||
|
||||
**Open tasks:**
|
||||
- · P4.1 - Evaluate Topaz as MVP delegated backend `9046418c`
|
||||
- · P4.2 - Add relationship PDP adapter boundary `b77a0b70`
|
||||
- · P4.3 - Add rule PDP adapter boundary `4e4e5e45`
|
||||
- · P4.4 - Add Keycloak Authorization Services adapter path `8d3bbc28`
|
||||
- · P4.5 - Add Entra/Graph and SCIM group resolver adapters `4fc3fb91`
|
||||
- · P4.6 - Add delegated-mode operations docs `491260f9`
|
||||
- ! T4 — Joint OpenBao + policy gate production smoke `32a96f1c`
|
||||
*(wait: Awaiting scoped VAULT_TOKEN refresh)*
|
||||
|
||||
---
|
||||
## MCP Orientation (when available)
|
||||
|
||||
If the state-hub MCP server is reachable, call:
|
||||
`get_domain_summary("netkingdom")`
|
||||
`get_domain_summary("infotech")`
|
||||
This provides richer cross-domain context.
|
||||
If the MCP call fails, use this file as your orientation source.
|
||||
|
||||
51
.github/workflows/ci.yml
vendored
Normal file
51
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build and Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
|
||||
- name: Verify dependencies
|
||||
run: go mod verify
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -race ./...
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir -p bin
|
||||
go build -o bin/flex-auth ./cmd/flex-auth
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,9 @@
|
||||
# ---> Go
|
||||
bin/
|
||||
*.test
|
||||
*.out
|
||||
coverage.txt
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
26
.golangci.yml
Normal file
26
.golangci.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unused
|
||||
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/netkingdom/flex-auth
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
25
.repo-classification.yaml
Normal file
25
.repo-classification.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
repo_classification:
|
||||
standard: Repo Classification Standard
|
||||
version: '1.0'
|
||||
classified_at: '2026-06-22'
|
||||
classified_by: human
|
||||
category: product
|
||||
domain: infotech
|
||||
secondary_domains:
|
||||
- government
|
||||
capability_tags:
|
||||
- identity
|
||||
- access-control
|
||||
- policy
|
||||
- governance
|
||||
- audit
|
||||
business_stake:
|
||||
- technology
|
||||
- legal
|
||||
- operations
|
||||
- product
|
||||
business_mechanics:
|
||||
- control
|
||||
- coordination
|
||||
- adaptation
|
||||
notes: Policy-as-code authorization registry; human corrected domain from communication→infotech.
|
||||
219
AGENTS.md
Normal file
219
AGENTS.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# flex-auth — Agent Instructions
|
||||
|
||||
## Repo Identity
|
||||
|
||||
**Purpose:** flex-auth - (fill in purpose)
|
||||
|
||||
**Domain:** infotech
|
||||
**Repo slug:** flex-auth
|
||||
**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a`
|
||||
**Workplan prefix:** `FLEX-WP-`
|
||||
|
||||
---
|
||||
|
||||
## State Hub Integration
|
||||
|
||||
The Custodian State Hub tracks work across all domains. Interact via HTTP REST —
|
||||
there is no MCP server for Codex agents.
|
||||
|
||||
| Context | URL |
|
||||
|---------|-----|
|
||||
| Local workstation | `http://127.0.0.1:8000` |
|
||||
| Remote via tunnel | `http://127.0.0.1:18000` |
|
||||
|
||||
### Orient at session start
|
||||
|
||||
```bash
|
||||
# Offline brief — works without hub connection
|
||||
cat .custodian-brief.md
|
||||
|
||||
# Active workstreams for this domain
|
||||
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
|
||||
| python3 -m json.tool
|
||||
|
||||
# Check inbox
|
||||
curl -s "http://127.0.0.1:8000/messages/?to_agent=flex-auth&unread_only=true" \
|
||||
| python3 -m json.tool
|
||||
```
|
||||
|
||||
Mark a message read:
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
||||
-H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
|
||||
### Log progress (required at session close)
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"summary": "what was done",
|
||||
"event_type": "note",
|
||||
"author": "codex",
|
||||
"workstream_id": "<uuid>",
|
||||
"task_id": "<uuid>"
|
||||
}'
|
||||
```
|
||||
|
||||
Omit `workstream_id` / `task_id` when not applicable.
|
||||
|
||||
### Update task status
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "progress"}'
|
||||
# values: wait | todo | progress | done | cancel
|
||||
```
|
||||
|
||||
### Flag a task for human review
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"needs_human": true, "intervention_note": "reason"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Protocol
|
||||
|
||||
**Start:**
|
||||
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
|
||||
2. Check inbox: `GET /messages/?to_agent=flex-auth&unread_only=true`; mark read
|
||||
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
|
||||
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
|
||||
|
||||
**During work:**
|
||||
- Update task statuses in workplan files as tasks progress
|
||||
- Record significant decisions via `POST /decisions/`
|
||||
|
||||
**Close:**
|
||||
1. Update workplan file task statuses to reflect progress
|
||||
2. Log: `POST /progress/` with a summary of what changed
|
||||
3. Note for the custodian operator: after workplan file changes, run from
|
||||
`~/state-hub`:
|
||||
```bash
|
||||
make fix-consistency REPO=flex-auth
|
||||
```
|
||||
This syncs task status from files into the hub DB.
|
||||
|
||||
---
|
||||
|
||||
## Credential and access routing
|
||||
|
||||
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
|
||||
for inference. Run this check **before** requesting secrets, API keys, SSH access,
|
||||
login tokens, or database passwords — in any repo, not only `ops-warden`.
|
||||
|
||||
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
|
||||
other credential need belongs to another subsystem. **Do not** message
|
||||
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
|
||||
|
||||
### Lookup (do this first)
|
||||
|
||||
```bash
|
||||
warden route find "<describe your need>" --json
|
||||
warden route show <catalog-id> --json
|
||||
```
|
||||
|
||||
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
|
||||
|
||||
| Agent runtime | How to orient |
|
||||
| --- | --- |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=flex-auth` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
|
||||
|
||||
### Quick routing table
|
||||
|
||||
| I need… | Owner | ops-warden executes? |
|
||||
| --- | --- | --- |
|
||||
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
|
||||
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
|
||||
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
|
||||
| Authorization decision | flex-auth | No — route only |
|
||||
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
|
||||
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
|
||||
|
||||
### Anti-patterns (do not do these)
|
||||
|
||||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
Non-credential capabilities are usually discovered through **reuse-surface** federation
|
||||
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
|
||||
every repo's agent instructions because it is high-frequency, high-risk, and easy to
|
||||
get wrong.
|
||||
|
||||
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
|
||||
|
||||
<!-- REPO-AGENTS-EXTENSIONS -->
|
||||
<!-- Append repo-specific agent instructions below this marker.
|
||||
The state-hub template sync preserves content after this line. -->
|
||||
|
||||
---
|
||||
|
||||
## Workplan Convention (ADR-001)
|
||||
|
||||
Work items originate as files in this repo — not in the hub. The hub is a
|
||||
read/cache/index layer that rebuilds from files.
|
||||
|
||||
**File location:** `workplans/FLEX-WP-NNNN-<slug>.md`
|
||||
|
||||
**Archived location:** finished workplans may move to
|
||||
`workplans/archived/YYMMDD-FLEX-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
|
||||
the completion/archive date; the frontmatter `id` does not change.
|
||||
|
||||
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
|
||||
`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use
|
||||
this only for low-risk work completed directly; create a normal workplan for
|
||||
anything needing analysis, design, approval, dependencies, or multiple phases.
|
||||
|
||||
**Frontmatter:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: FLEX-WP-NNNN
|
||||
type: workplan
|
||||
title: "..."
|
||||
domain: infotech
|
||||
repo: flex-auth
|
||||
status: proposed | ready | active | blocked | backlog | finished | archived
|
||||
owner: codex
|
||||
topic_slug: ...
|
||||
created: "YYYY-MM-DD"
|
||||
updated: "YYYY-MM-DD"
|
||||
state_hub_workstream_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
---
|
||||
```
|
||||
|
||||
Use `proposed` for a new draft, `ready` after review against current repo
|
||||
state, and `finished` after implementation. `stalled` and `needs_review` are
|
||||
derived health labels, not frontmatter statuses.
|
||||
|
||||
**Task block format** (one per `##` section):
|
||||
|
||||
```
|
||||
## Task Title
|
||||
|
||||
` ` `task
|
||||
id: FLEX-WP-NNNN-T01
|
||||
status: wait | todo | progress | done | cancel
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
` ` `
|
||||
|
||||
Task description text.
|
||||
```
|
||||
|
||||
Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
|
||||
|
||||
To create a new workplan:
|
||||
1. Write the file following the format above
|
||||
2. Notify the custodian operator to run `make fix-consistency REPO=flex-auth`
|
||||
(or send a message to the hub agent via `POST /messages/`)
|
||||
12
CLAUDE.md
Normal file
12
CLAUDE.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# flex-auth — Claude Code Instructions
|
||||
|
||||
@SCOPE.md
|
||||
@.claude/rules/repo-identity.md
|
||||
@.claude/rules/session-protocol.md
|
||||
@.claude/rules/first-session.md
|
||||
@.claude/rules/workplan-convention.md
|
||||
@.claude/rules/stack-and-commands.md
|
||||
@.claude/rules/architecture.md
|
||||
@.claude/rules/repo-boundary.md
|
||||
@.claude/rules/credential-routing.md
|
||||
@.claude/rules/agents.md
|
||||
119
INTENT.md
119
INTENT.md
@@ -1,6 +1,9 @@
|
||||
# Flex-Auth Intent
|
||||
|
||||
Date: 2026-05-04
|
||||
> This file captures **why this repository exists**, the **direction it is
|
||||
> moving toward**, and the **kind of system it is meant to become**.
|
||||
> It is intentionally **aspirational and stable**, not a description of
|
||||
> current implementation.
|
||||
|
||||
## Intent
|
||||
|
||||
@@ -9,20 +12,19 @@ organizations that want to grow from simple access rules into enterprise-grade
|
||||
authorization without giving up clear ownership, local development ergonomics,
|
||||
or inspectable policy decisions.
|
||||
|
||||
It belongs in the NetKingdom tooling landscape as the authorization counterpart
|
||||
to key-cape/NetKingdom identity:
|
||||
It is the **authorization layer** in the path from verified identity to
|
||||
protected resources:
|
||||
|
||||
```text
|
||||
key-cape / NetKingdom SSO
|
||||
-> verified OIDC/SAML identity claims
|
||||
verified identity claims
|
||||
-> flex-auth policy-as-code and authorization registry
|
||||
-> protected systems and knowledge tools
|
||||
-> protected systems and their resources
|
||||
```
|
||||
|
||||
Flex-auth should run usefully on its own, but it should also delegate to or
|
||||
coordinate with established authorization engines such as Topaz, OpenFGA,
|
||||
SpiceDB, OPA, Cedar, Keycloak Authorization Services, and enterprise directory
|
||||
systems.
|
||||
Flex-auth should run usefully on its own, and should also be able to delegate
|
||||
to or coordinate established authorization engines — relationship/graph
|
||||
engines, rule and attribute policy engines, and directory systems — without
|
||||
binding its own model to any one of them.
|
||||
|
||||
## Why This Exists
|
||||
|
||||
@@ -36,7 +38,7 @@ conditionals. Over time they need richer policy:
|
||||
- emergency/break-glass controls
|
||||
- policy tests and reviewable changes
|
||||
- durable decision logs and explainability
|
||||
- integration with SSO, MFA, service accounts, and directory groups
|
||||
- integration with identity, MFA, service accounts, and directory groups
|
||||
|
||||
Flex-auth should give those organizations a path that starts small and grows
|
||||
cleanly instead of forcing an early leap into a large IAM platform or letting
|
||||
@@ -44,14 +46,17 @@ authorization logic sprawl across applications.
|
||||
|
||||
## Responsibility Boundary
|
||||
|
||||
### key-cape / NetKingdom Owns Identity
|
||||
Flex-auth consumes **verified identity claims** as normative input and never
|
||||
re-defines them. Identity proves who an actor is and how they were
|
||||
authenticated; flex-auth decides what that actor is allowed to do.
|
||||
|
||||
- OIDC discovery and token issuance.
|
||||
- Human login, MFA, PKCE, service accounts, token lifecycle.
|
||||
- Canonical IAM profile and required claims.
|
||||
- Coarse app roles/scopes and assurance claims.
|
||||
### The Identity Layer Owns Identity
|
||||
|
||||
### flex-auth Owns Authorization
|
||||
- Authentication, login, MFA, and token issuance and lifecycle.
|
||||
- The canonical identity claim contract and required claims.
|
||||
- Coarse roles, scopes, and assurance claims.
|
||||
|
||||
### Flex-Auth Owns Authorization
|
||||
|
||||
- Protected-system registration.
|
||||
- Resource namespaces and resource hierarchy.
|
||||
@@ -65,17 +70,17 @@ authorization logic sprawl across applications.
|
||||
|
||||
### Protected Systems Own Enforcement
|
||||
|
||||
Applications such as Markitect remain policy enforcement points. They extract
|
||||
resource metadata, call flex-auth for decisions, enforce allow/deny/redact
|
||||
results, and emit local diagnostics. They do not own central enterprise policy
|
||||
administration.
|
||||
Applications remain policy enforcement points. They extract resource
|
||||
metadata, call flex-auth for decisions, enforce allow/deny/redact results,
|
||||
and emit local diagnostics. They do not own central policy administration.
|
||||
|
||||
## Design Principles
|
||||
|
||||
- Policy is code: versioned, reviewed, tested, and explainable.
|
||||
- Identity is not authorization: SSO claims are inputs, not final decisions.
|
||||
- Identity is not authorization: identity claims are inputs, not final
|
||||
decisions.
|
||||
- Start standalone, scale outward: a local flex-auth deployment should be
|
||||
useful before Topaz/OpenFGA/OPA integrations are available.
|
||||
useful before any external policy engine integration is available.
|
||||
- Backend-neutral core: flex-auth has its own resource, action, request,
|
||||
decision, and audit vocabulary.
|
||||
- Pluggable PDPs: relationship, rule, and directory engines are adapters, not
|
||||
@@ -83,7 +88,7 @@ administration.
|
||||
- Fail visibly: denied, redacted, stale, partial, and uncertain decisions must
|
||||
produce useful diagnostics.
|
||||
- Grow into enterprise: the same model should support local dev, small teams,
|
||||
NetKingdom-managed deployments, and larger Keycloak/Entra environments.
|
||||
and larger enterprise environments.
|
||||
|
||||
## First-Class Concepts
|
||||
|
||||
@@ -126,40 +131,62 @@ Standalone flex-auth should provide:
|
||||
- deterministic check and batch-check APIs
|
||||
- local decision log
|
||||
- CLI and service mode
|
||||
- test fixtures for Keycloak/key-cape-like claims
|
||||
- test fixtures for representative identity claims
|
||||
|
||||
Standalone mode should be enough for development, smaller deployments, and
|
||||
integration tests.
|
||||
|
||||
## Delegated Mode
|
||||
|
||||
Delegated mode should let flex-auth coordinate established systems:
|
||||
Delegated mode should let flex-auth coordinate established systems without
|
||||
adopting their models as its own:
|
||||
|
||||
- Topaz for a local directory plus OPA/Rego policy evaluation.
|
||||
- OpenFGA or SpiceDB for graph-heavy relationship authorization.
|
||||
- OPA or Cedar for attribute/rule policies.
|
||||
- Keycloak Authorization Services for Keycloak-centric deployments.
|
||||
- Microsoft Graph or SCIM/LDAP/Keycloak APIs for directory group resolution.
|
||||
- relationship/graph engines for relationship-heavy authorization
|
||||
- rule and attribute policy engines for attribute/rule policies
|
||||
- directory systems for group resolution
|
||||
- a local directory plus policy-evaluation engine for self-contained
|
||||
delegated setups
|
||||
|
||||
Flex-auth remains the stable control plane even when the PDP backend changes.
|
||||
Flex-auth remains the stable control plane even when the backend changes.
|
||||
|
||||
## First Consumer: Markitect
|
||||
## Consumer Patterns
|
||||
|
||||
Markitect is the first concrete consumer:
|
||||
Two consumer shapes drive flex-auth, and the first one to ship deliberately
|
||||
is not a document pipeline — proving the control plane stays generic.
|
||||
|
||||
- Markitect registers knowledge bases, repositories, documents, sections,
|
||||
context packages, workflow artifacts, and exports.
|
||||
- Markitect sends policy checks before returning query/search/context results.
|
||||
- Markitect can redact or drop results based on decisions.
|
||||
- flex-auth owns central policy administration and durable audit.
|
||||
**First shipped consumer — an action gate (ops-warden SSH signing).** A
|
||||
protected system asks flex-auth a single "may this actor perform this action
|
||||
now?" question before doing irreversible work:
|
||||
|
||||
This first consumer should shape flex-auth around real Markdown knowledge
|
||||
pipelines without making the policy service Markitect-specific.
|
||||
- it registers a protected system, a resource type (`ssh-certificate`), and an
|
||||
action (`sign`)
|
||||
- it sends one policy check per request, passing subject, resource, and
|
||||
context (actor type, principals, TTL, key fingerprint)
|
||||
- it enforces the allow/deny decision and records the decision id for audit
|
||||
- flex-auth owns the policy and durable decision log; the protected system
|
||||
keeps custody of its own keys and secrets
|
||||
|
||||
This first consumer validated that flex-auth's resource/action/context model,
|
||||
`POST /v1/check` contract, and decision envelope work for a non-document,
|
||||
high-stakes gate without any consumer-specific routes.
|
||||
|
||||
**First knowledge-pipeline consumer (planned) — a document and knowledge
|
||||
pipeline (Markitect):**
|
||||
|
||||
- it registers knowledge bases, repositories, documents, sections, context
|
||||
packages, workflow artifacts, and exports
|
||||
- it sends policy checks before returning query/search/context results
|
||||
- it can redact or drop results based on decisions
|
||||
- flex-auth owns central policy administration and durable audit
|
||||
|
||||
Together these shape flex-auth around real authorization needs — both
|
||||
point-in-time action gates and result-filtering pipelines — without making the
|
||||
policy service consumer-specific.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Flex-auth is not an identity provider.
|
||||
- Flex-auth is not a replacement for key-cape or NetKingdom SSO.
|
||||
- Flex-auth is not a replacement for an identity or SSO system.
|
||||
- Flex-auth is not a mandatory dependency for every local development use case.
|
||||
- Flex-auth should not force one PDP backend.
|
||||
- Flex-auth should not hide policy complexity behind opaque admin toggles.
|
||||
@@ -169,7 +196,7 @@ pipelines without making the policy service Markitect-specific.
|
||||
1. Define the resource/action/decision model.
|
||||
2. Define policy package structure and test fixtures.
|
||||
3. Implement standalone registry and check API.
|
||||
4. Add Markitect resource manifest and policy adapter.
|
||||
5. Evaluate Topaz as the first delegated backend.
|
||||
6. Add OpenFGA/SpiceDB and OPA adapter spikes.
|
||||
7. Add Keycloak/key-cape and Entra integration examples.
|
||||
4. Add a first protected-system resource manifest and policy adapter.
|
||||
5. Evaluate a delegated directory-plus-policy backend.
|
||||
6. Add relationship-engine and rule-engine adapter spikes.
|
||||
7. Add identity and enterprise-directory integration examples.
|
||||
|
||||
59
Makefile
Normal file
59
Makefile
Normal file
@@ -0,0 +1,59 @@
|
||||
BIN_DIR ?= bin
|
||||
BIN := $(BIN_DIR)/flex-auth
|
||||
PKG := ./...
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo 0.0.0-dev)
|
||||
LDFLAGS := -X main.version=$(VERSION)
|
||||
|
||||
.PHONY: all build test vet lint fmt tidy sbom clean ci
|
||||
|
||||
all: vet lint test build
|
||||
|
||||
build:
|
||||
@mkdir -p $(BIN_DIR)
|
||||
go build -ldflags "$(LDFLAGS)" -o $(BIN) ./cmd/flex-auth
|
||||
|
||||
test:
|
||||
go test -race $(PKG)
|
||||
|
||||
vet:
|
||||
go vet $(PKG)
|
||||
|
||||
fmt:
|
||||
gofmt -l -w .
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
lint:
|
||||
@if command -v golangci-lint >/dev/null 2>&1; then \
|
||||
golangci-lint run $(PKG); \
|
||||
else \
|
||||
echo "golangci-lint not installed; run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
|
||||
echo "falling back to: go vet"; \
|
||||
go vet $(PKG); \
|
||||
fi
|
||||
|
||||
GOBIN_PATH := $(shell go env GOPATH)/bin
|
||||
|
||||
sbom:
|
||||
@mkdir -p $(BIN_DIR)
|
||||
@if command -v cyclonedx-gomod >/dev/null 2>&1; then \
|
||||
cyclonedx-gomod mod -json -output $(BIN_DIR)/sbom.cdx.json .; \
|
||||
echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (cyclonedx-gomod)"; \
|
||||
elif [ -x "$(GOBIN_PATH)/cyclonedx-gomod" ]; then \
|
||||
"$(GOBIN_PATH)/cyclonedx-gomod" mod -json -output $(BIN_DIR)/sbom.cdx.json .; \
|
||||
echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (cyclonedx-gomod via GOPATH)"; \
|
||||
elif command -v syft >/dev/null 2>&1; then \
|
||||
syft . -o cyclonedx-json=$(BIN_DIR)/sbom.cdx.json; \
|
||||
echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (syft)"; \
|
||||
else \
|
||||
echo "no SBOM tool found. install one:"; \
|
||||
echo " go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest"; \
|
||||
echo " or syft: https://github.com/anchore/syft"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
clean:
|
||||
rm -rf $(BIN_DIR)
|
||||
|
||||
ci: vet lint test build
|
||||
13
README.md
13
README.md
@@ -4,11 +4,18 @@ Policy-as-code authorization registry and control plane for NetKingdom-aligned
|
||||
systems.
|
||||
|
||||
Start with [INTENT.md](INTENT.md) for the project boundary and direction.
|
||||
Research notes live in [docs/](docs/).
|
||||
Research notes and ADRs live in [docs/](docs/) and [docs/adr/](docs/adr/).
|
||||
|
||||
The product boundary is captured in [SCOPE.md](SCOPE.md), and the current
|
||||
Product Requirements Document is
|
||||
[docs/ProductRequirementsDocument.md](docs/ProductRequirementsDocument.md).
|
||||
|
||||
Initial workplans live in [workplans/](workplans/), with sequencing captured
|
||||
in [docs/workplan-planning-map.md](docs/workplan-planning-map.md).
|
||||
The 2026-05-15 pre-implementation assessment that shapes the current
|
||||
sequencing is in
|
||||
[docs/pre-implementation-assessment.md](docs/pre-implementation-assessment.md).
|
||||
|
||||
The CARING reference-implementation approach is captured in
|
||||
[docs/caring-architecture-blueprint.md](docs/caring-architecture-blueprint.md).
|
||||
|
||||
Workplans live in [workplans/](workplans/), with sequencing captured in
|
||||
[docs/workplan-planning-map.md](docs/workplan-planning-map.md).
|
||||
|
||||
57
SCOPE.md
57
SCOPE.md
@@ -72,11 +72,22 @@ can be coordinated behind a stable flex-auth API.
|
||||
|
||||
## Current State
|
||||
|
||||
The repository contains the intent baseline, authorization landscape research,
|
||||
and initial workplans. `FLEX-WP-0001` is complete. Current implementation work
|
||||
starts with `FLEX-WP-0002`, the standalone policy-as-code core. Markitect
|
||||
consumer integration and delegated PDP/directory adapters are planned after
|
||||
the core contracts stabilize.
|
||||
The standalone core is implemented. The repository carries the intent
|
||||
baseline, authorization landscape research, ADR set, and a working Go
|
||||
service (`cmd/flex-auth`) with `validate`, `load-registry`, `serve`, and
|
||||
`POST /v1/check` plus registry, policy, decision, and audit internals.
|
||||
`FLEX-WP-0001`, `FLEX-WP-0005` (foundations and Topaz alignment), and
|
||||
`FLEX-WP-0006` (the ops-warden SSH signing policy gate) are complete.
|
||||
|
||||
The **first shipped protected-system consumer is ops-warden**: its opt-in
|
||||
pre-sign gate calls `POST /v1/check` for `resource.type: ssh-certificate`,
|
||||
`action: sign` decisions (`examples/ops-warden/`, policy package, allow/deny
|
||||
fixtures, and tests). `FLEX-WP-0007` deploys flex-auth as a reachable
|
||||
production runtime for that gate; it is `blocked` only on T4 — the joint
|
||||
OpenBao-backed smoke awaiting a refreshed scoped `VAULT_TOKEN` — with all
|
||||
repo-side artifacts already published. Markitect consumer integration
|
||||
(`FLEX-WP-0003`) and delegated PDP/directory adapters (`FLEX-WP-0004`)
|
||||
remain planned on top of the stable core contracts.
|
||||
|
||||
State Hub integration is present through:
|
||||
|
||||
@@ -120,17 +131,43 @@ local diagnostics.
|
||||
|
||||
## Related / Overlapping
|
||||
|
||||
- key-cape / NetKingdom SSO: identity source and coarse claims provider.
|
||||
- Markitect: first protected-system consumer and policy enforcement point.
|
||||
- Topaz: candidate MVP delegated backend combining local directory and
|
||||
OPA/Rego evaluation.
|
||||
- key-cape / NetKingdom SSO: identity source and coarse claims provider;
|
||||
flex-auth consumes the **NetKingdom IAM Profile**
|
||||
(`~/net-kingdom/canon/standards/iam-profile_v0.2.md`).
|
||||
- ops-warden: first **shipped** protected-system consumer. Its opt-in
|
||||
pre-sign gate calls flex-auth for `ssh-certificate` / `sign` decisions
|
||||
before issuing a short-lived SSH certificate (`FLEX-WP-0006`,
|
||||
`FLEX-WP-0007`). ops-warden owns the SSH CA, OpenBao signing, and actor
|
||||
inventory; flex-auth owns the policy decision. ops-warden's routing
|
||||
charter names flex-auth as the owner of every "may I perform action X?"
|
||||
question.
|
||||
- Markitect: first planned **knowledge-pipeline** consumer and policy
|
||||
enforcement point (`FLEX-WP-0003`).
|
||||
- Topaz: aligned evaluator. Per ADR-003 the standalone core is shaped
|
||||
to match Topaz's Rego + directory model from day one; the Topaz
|
||||
adapter in `FLEX-WP-0004` is therefore a small step rather than a
|
||||
conversion.
|
||||
- OpenFGA and SpiceDB: candidate relationship authorization backends.
|
||||
- OPA and Cedar: candidate rule and typed-policy engines.
|
||||
- Keycloak Authorization Services: adapter path for Keycloak-centric
|
||||
deployments.
|
||||
deployments. Default architecture is "Keycloak as SSO only,
|
||||
flex-auth owns authorization"; Keycloak AuthZ is one optional
|
||||
delegated PDP.
|
||||
- Entra, Graph, SCIM, LDAP, and Keycloak APIs: directory and group resolver
|
||||
sources.
|
||||
|
||||
## Disjoint From
|
||||
|
||||
- **ops-warden** is a flex-auth *consumer*, not an overlap (see Related /
|
||||
Overlapping). The two remain disjoint on **identity surface**: ops-warden
|
||||
issues SSH certificates for ops actors (`adm`/`agt`/`atm`) and is not a
|
||||
resource-policy engine; flex-auth decides whether a given sign request is
|
||||
allowed and never issues certificates. The once-hypothetical flow of
|
||||
surfacing an `agt` actor as a flex-auth subject is now realized through
|
||||
the signing policy gate.
|
||||
- **ops-bridge** owns SSH reverse-tunnel connectivity and explicitly
|
||||
disclaims being a credential authority or policy engine. No overlap.
|
||||
|
||||
## Provided Capabilities
|
||||
|
||||
```capability
|
||||
|
||||
505
cmd/flex-auth/main.go
Normal file
505
cmd/flex-auth/main.go
Normal file
@@ -0,0 +1,505 @@
|
||||
// Command flex-auth is the CLI entry point for the flex-auth authorization
|
||||
// registry and control plane.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/netkingdom/flex-auth/internal/audit"
|
||||
decisioncore "github.com/netkingdom/flex-auth/internal/decision"
|
||||
"github.com/netkingdom/flex-auth/internal/policy"
|
||||
"github.com/netkingdom/flex-auth/internal/registry"
|
||||
"github.com/netkingdom/flex-auth/pkg/api"
|
||||
)
|
||||
|
||||
// version is set at build time via -ldflags "-X main.version=...".
|
||||
var version = "0.0.0-dev"
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
func run(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
printUsage(stderr)
|
||||
return 64
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "version", "--version", "-version":
|
||||
fmt.Fprintln(stdout, version)
|
||||
return 0
|
||||
case "validate":
|
||||
return runValidate(args[1:], stdout, stderr)
|
||||
case "load-registry":
|
||||
return runLoadRegistry(args[1:], stdout, stderr)
|
||||
case "test-policy":
|
||||
return runTestPolicy(args[1:], stdout, stderr)
|
||||
case "check":
|
||||
return runCheck(args[1:], stdout, stderr)
|
||||
case "batch-check":
|
||||
return runBatchCheck(args[1:], stdout, stderr)
|
||||
case "list-allowed":
|
||||
return runListAllowed(args[1:], stdout, stderr)
|
||||
case "explain":
|
||||
return runExplain(args[1:], stdout, stderr)
|
||||
case "serve":
|
||||
return runServe(args[1:], stdout, stderr)
|
||||
case "help", "-h", "--help":
|
||||
printUsage(stdout)
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintf(stderr, "flex-auth: unknown subcommand %q\n", args[0])
|
||||
printUsage(stderr)
|
||||
return 64
|
||||
}
|
||||
}
|
||||
|
||||
func runValidate(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("validate", stderr)
|
||||
kind := fs.String("kind", "", "resource-manifest, subject-manifest, protected-system, relationship, access-descriptor, or policy")
|
||||
file := fs.String("file", "", "YAML/JSON file to validate")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *kind == "" || *file == "" {
|
||||
fmt.Fprintln(stderr, "validate requires --kind and --file")
|
||||
return 64
|
||||
}
|
||||
|
||||
switch *kind {
|
||||
case "resource-manifest":
|
||||
var manifest api.ResourceManifest
|
||||
if err := readYAML(*file, &manifest); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := registry.NewStore().ImportResourceManifest(manifest); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
case "subject-manifest":
|
||||
var manifest api.SubjectManifest
|
||||
if err := readYAML(*file, &manifest); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := registry.NewStore().ImportSubjectManifest(manifest); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
case "protected-system":
|
||||
var manifest api.ProtectedSystemManifest
|
||||
if err := readYAML(*file, &manifest); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := registry.NewStore().PutProtectedSystem(manifest); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
case "relationship":
|
||||
var fact api.RelationshipFact
|
||||
if err := readYAML(*file, &fact); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := registry.NewStore().PutRelationship(fact); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
case "access-descriptor":
|
||||
var descriptor api.CaringAccessDescriptor
|
||||
if err := readYAML(*file, &descriptor); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := validateDescriptor(descriptor); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
case "policy", "policy-package":
|
||||
pkg, err := policy.LoadAndValidateFile(context.Background(), *file)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := writeJSON(stdout, pkg.Validation); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if !pkg.Valid {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintf(stderr, "unsupported validate kind %q\n", *kind)
|
||||
return 64
|
||||
}
|
||||
|
||||
return writeStatus(stdout, "valid", map[string]any{"kind": *kind, "file": *file})
|
||||
}
|
||||
|
||||
func runLoadRegistry(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("load-registry", stderr)
|
||||
file := fs.String("file", "", "registry snapshot JSON file")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *file == "" {
|
||||
fmt.Fprintln(stderr, "load-registry requires --file")
|
||||
return 64
|
||||
}
|
||||
|
||||
store, err := registry.LoadFile(*file)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
snapshot := store.Snapshot()
|
||||
return writeStatus(stdout, "loaded", map[string]any{
|
||||
"file": *file,
|
||||
"systems": len(snapshot.Systems),
|
||||
"resource_manifests": len(snapshot.ResourceManifests),
|
||||
"subjects": len(snapshot.Subjects),
|
||||
"groups": len(snapshot.Groups),
|
||||
"teams": len(snapshot.Teams),
|
||||
"tenants": len(snapshot.Tenants),
|
||||
"relationships": len(snapshot.Relationships),
|
||||
})
|
||||
}
|
||||
|
||||
func runTestPolicy(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("test-policy", stderr)
|
||||
file := fs.String("file", "", "policy package Markdown file")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *file == "" {
|
||||
fmt.Fprintln(stderr, "test-policy requires --file")
|
||||
return 64
|
||||
}
|
||||
|
||||
pkg, err := policy.LoadAndValidateFile(context.Background(), *file)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := writeJSON(stdout, pkg.Validation); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if !pkg.Valid {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runCheck(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("check", stderr)
|
||||
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||
requestPath := fs.String("request", "", "check request YAML/JSON file")
|
||||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *registryPath == "" || *policyPath == "" || *requestPath == "" {
|
||||
fmt.Fprintln(stderr, "check requires --registry, --policy, and --request")
|
||||
return 64
|
||||
}
|
||||
|
||||
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
var request api.CheckRequest
|
||||
if err := readYAML(*requestPath, &request); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
decision, err := engine.Check(context.Background(), request)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := writeJSON(stdout, decision); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runBatchCheck(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("batch-check", stderr)
|
||||
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||
requestPath := fs.String("request", "", "batch check request YAML/JSON file")
|
||||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *registryPath == "" || *policyPath == "" || *requestPath == "" {
|
||||
fmt.Fprintln(stderr, "batch-check requires --registry, --policy, and --request")
|
||||
return 64
|
||||
}
|
||||
|
||||
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
var request api.BatchCheckRequest
|
||||
if err := readYAML(*requestPath, &request); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
decisions, err := engine.BatchCheck(context.Background(), request)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := writeJSON(stdout, decisions); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runListAllowed(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("list-allowed", stderr)
|
||||
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||
subject := fs.String("subject", "", "subject id")
|
||||
action := fs.String("action", "", "action name")
|
||||
system := fs.String("system", "", "protected system id")
|
||||
resourceType := fs.String("resource-type", "", "resource type")
|
||||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
var filters keyValueFlags
|
||||
fs.Var(&filters, "filter", "resource filter as key=value; may be repeated")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *registryPath == "" || *policyPath == "" || *subject == "" || *action == "" {
|
||||
fmt.Fprintln(stderr, "list-allowed requires --registry, --policy, --subject, and --action")
|
||||
return 64
|
||||
}
|
||||
|
||||
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
decisions, err := engine.ListAllowed(context.Background(), decisioncore.ListAllowedRequest{
|
||||
Subject: api.SubjectRef{ID: *subject},
|
||||
Action: *action,
|
||||
System: *system,
|
||||
ResourceType: *resourceType,
|
||||
Filters: filters.Map(),
|
||||
})
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := writeJSON(stdout, decisions); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runExplain(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("explain", stderr)
|
||||
logPath := fs.String("decision-log", "", "JSONL decision log path")
|
||||
decisionID := fs.String("decision-id", "", "decision id to explain")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *logPath == "" || *decisionID == "" {
|
||||
fmt.Fprintln(stderr, "explain requires --decision-log and --decision-id")
|
||||
return 64
|
||||
}
|
||||
|
||||
log := audit.NewJSONLDecisionLog(*logPath)
|
||||
envelope, ok, err := log.Find(*decisionID)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if !ok {
|
||||
return fail(stderr, fmt.Errorf("decision %q not found", *decisionID))
|
||||
}
|
||||
if err := writeJSON(stdout, decisioncore.ExplainEnvelope(envelope)); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runServe(args []string, stdout, stderr io.Writer) int {
|
||||
fs := newFlagSet("serve", stderr)
|
||||
addr := fs.String("addr", "127.0.0.1:8080", "HTTP listen address")
|
||||
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 64
|
||||
}
|
||||
if *registryPath == "" || *policyPath == "" {
|
||||
fmt.Fprintln(stderr, "serve requires --registry and --policy")
|
||||
return 64
|
||||
}
|
||||
|
||||
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
|
||||
mux := newServeMux(engine)
|
||||
|
||||
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
|
||||
if err := http.ListenAndServe(*addr, mux); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func newServeMux(engine *decisioncore.Engine) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
})
|
||||
mux.HandleFunc("/v1/check", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var request api.CheckRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
decision, err := engine.Check(r.Context(), request)
|
||||
writeHTTP(w, decision, err)
|
||||
})
|
||||
mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var request api.BatchCheckRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
decisions, err := engine.BatchCheck(r.Context(), request)
|
||||
writeHTTP(w, decisions, err)
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) {
|
||||
store, err := registry.LoadFile(registryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg, err := policy.LoadAndValidateFile(ctx, policyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine, err := decisioncore.NewEngine(store, pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if logPath != "" {
|
||||
engine.SetDecisionLog(audit.NewJSONLDecisionLog(logPath))
|
||||
}
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func validateDescriptor(descriptor api.CaringAccessDescriptor) error {
|
||||
if descriptor.Profile != api.CaringProfileCaring040RC2 {
|
||||
return fmt.Errorf("unsupported CARING profile %q", descriptor.Profile)
|
||||
}
|
||||
if descriptor.SubjectType == "" {
|
||||
return fmt.Errorf("subject_type is required")
|
||||
}
|
||||
if descriptor.OrganizationRelation == "" {
|
||||
return fmt.Errorf("organization_relation is required")
|
||||
}
|
||||
if descriptor.CanonicalRole == "" {
|
||||
return fmt.Errorf("canonical_role is required")
|
||||
}
|
||||
if descriptor.Scope.Level == "" || descriptor.Scope.ID == "" {
|
||||
return fmt.Errorf("scope.level and scope.id are required")
|
||||
}
|
||||
if len(descriptor.Planes) == 0 {
|
||||
return fmt.Errorf("at least one plane is required")
|
||||
}
|
||||
if len(descriptor.Capabilities) == 0 {
|
||||
return fmt.Errorf("at least one capability is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readYAML(path string, out any) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := yaml.Unmarshal(data, out); err != nil {
|
||||
return fmt.Errorf("unmarshal %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(w io.Writer, value any) error {
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeStatus(w io.Writer, status string, extra map[string]any) int {
|
||||
out := map[string]any{"status": status}
|
||||
for key, value := range extra {
|
||||
out[key] = value
|
||||
}
|
||||
if err := writeJSON(w, out); err != nil {
|
||||
return fail(w, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func writeHTTP(w http.ResponseWriter, value any, err error) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
|
||||
func newFlagSet(name string, stderr io.Writer) *flag.FlagSet {
|
||||
fs := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
return fs
|
||||
}
|
||||
|
||||
func fail(stderr io.Writer, err error) int {
|
||||
fmt.Fprintln(stderr, "flex-auth:", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
func printUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "usage: flex-auth <command> [options]")
|
||||
fmt.Fprintln(w, "commands: version, validate, load-registry, test-policy, check, batch-check, list-allowed, explain, serve")
|
||||
}
|
||||
|
||||
type keyValueFlags []string
|
||||
|
||||
func (f *keyValueFlags) String() string {
|
||||
return strings.Join(*f, ",")
|
||||
}
|
||||
|
||||
func (f *keyValueFlags) Set(value string) error {
|
||||
if !strings.Contains(value, "=") {
|
||||
return fmt.Errorf("filter must be key=value")
|
||||
}
|
||||
*f = append(*f, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f keyValueFlags) Map() map[string]any {
|
||||
out := make(map[string]any, len(f))
|
||||
for _, item := range f {
|
||||
key, value, _ := strings.Cut(item, "=")
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
340
cmd/flex-auth/main_test.go
Normal file
340
cmd/flex-auth/main_test.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/netkingdom/flex-auth/pkg/api"
|
||||
)
|
||||
|
||||
func TestRunVersion(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{"version"}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
if strings.TrimSpace(stdout.String()) == "" {
|
||||
t.Fatal("version output is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunTestPolicy(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{"test-policy", "--file", examplePath("policy_package.md")}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), `"valid": true`) {
|
||||
t.Fatalf("stdout = %s; want valid policy result", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheck(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{
|
||||
"check",
|
||||
"--registry", examplePath("registry_snapshot.json"),
|
||||
"--policy", examplePath("policy_package.md"),
|
||||
"--request", examplePath("check_request.yaml"),
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
|
||||
var decision api.DecisionEnvelope
|
||||
if err := json.Unmarshal(stdout.Bytes(), &decision); err != nil {
|
||||
t.Fatalf("unmarshal decision: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if decision.Effect != api.DecisionEffectAllow {
|
||||
t.Fatalf("decision.Effect = %q; want allow", decision.Effect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBatchCheck(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{
|
||||
"batch-check",
|
||||
"--registry", examplePath("registry_snapshot.json"),
|
||||
"--policy", examplePath("policy_package.md"),
|
||||
"--request", examplePath("batch_check_request.yaml"),
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
|
||||
var decisions []api.DecisionEnvelope
|
||||
if err := json.Unmarshal(stdout.Bytes(), &decisions); err != nil {
|
||||
t.Fatalf("unmarshal decisions: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if len(decisions) != 2 || decisions[0].Effect != api.DecisionEffectAllow || decisions[1].Effect != api.DecisionEffectDeny {
|
||||
t.Fatalf("decisions = %+v; want allow then deny", decisions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckOpsWarden(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{
|
||||
"check",
|
||||
"--registry", opsPath("registry_snapshot.json"),
|
||||
"--policy", opsPath("policy_package.md"),
|
||||
"--request", opsPath("check_request_allow_adm.json"),
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
|
||||
var decision api.DecisionEnvelope
|
||||
if err := json.Unmarshal(stdout.Bytes(), &decision); err != nil {
|
||||
t.Fatalf("unmarshal decision: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if decision.Effect != api.DecisionEffectAllow {
|
||||
t.Fatalf("decision.Effect = %q; want allow", decision.Effect)
|
||||
}
|
||||
if decision.ID == "" {
|
||||
t.Fatal("decision.ID is empty; ops-warden needs a policy_decision_id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeOpsWardenCheckContract(t *testing.T) {
|
||||
logPath := filepath.Join(t.TempDir(), "decisions.jsonl")
|
||||
engine, err := buildEngine(context.Background(), opsPath("registry_snapshot.json"), opsPath("policy_package.md"), logPath)
|
||||
if err != nil {
|
||||
t.Fatalf("buildEngine: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(newServeMux(engine))
|
||||
defer server.Close()
|
||||
|
||||
resp, err := http.Get(server.URL + "/healthz")
|
||||
if err != nil {
|
||||
t.Fatalf("GET /healthz: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("GET /healthz status = %d; want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
allow := postCheck(t, server.URL+"/v1/check", opsPath("check_request_allow_adm.json"))
|
||||
if allow.Effect != api.DecisionEffectAllow || allow.ID == "" {
|
||||
t.Fatalf("allow decision = %+v; want allow with id", allow)
|
||||
}
|
||||
|
||||
deny := postCheck(t, server.URL+"/v1/check", opsPath("check_request_deny_ttl_above_max.json"))
|
||||
if deny.Effect != api.DecisionEffectDeny || deny.Reason != "ttl_out_of_bounds" {
|
||||
t.Fatalf("deny decision = %+v; want ttl_out_of_bounds deny", deny)
|
||||
}
|
||||
|
||||
resp, err = http.Get(server.URL + "/v1/check")
|
||||
if err != nil {
|
||||
t.Fatalf("GET /v1/check: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("GET /v1/check status = %d; want 405", resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, err = http.Post(server.URL+"/v1/check", "application/json", strings.NewReader(`{"subject":`))
|
||||
if err != nil {
|
||||
t.Fatalf("POST malformed /v1/check: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("malformed POST status = %d; want 400", resp.StatusCode)
|
||||
}
|
||||
|
||||
logData, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read decision log: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(logData), allow.ID) || !strings.Contains(string(logData), deny.ID) {
|
||||
t.Fatalf("decision log does not contain both decision ids\nlog: %s\nallow: %s deny: %s", string(logData), allow.ID, deny.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoadRegistryOpsWardenProduction(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{"load-registry", "--file", opsPath("production_registry_snapshot.json")}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
||||
t.Fatalf("unmarshal load-registry output: %v; stdout = %s", err, stdout.String())
|
||||
}
|
||||
if result["subjects"] != float64(4) || result["relationships"] != float64(4) || result["resource_manifests"] != float64(1) {
|
||||
t.Fatalf("load-registry result = %+v; want production actor registry counts", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpsWardenProductionRegistryActors(t *testing.T) {
|
||||
engine, err := buildEngine(context.Background(), opsPath("production_registry_snapshot.json"), opsPath("policy_package.md"), "")
|
||||
if err != nil {
|
||||
t.Fatalf("buildEngine: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
subjectID string
|
||||
actor string
|
||||
actorType string
|
||||
principal string
|
||||
ttlHours float64
|
||||
wantEffect api.DecisionEffect
|
||||
wantReason string
|
||||
}{
|
||||
{
|
||||
name: "state hub bridge agent allow",
|
||||
subjectID: "agt-state-hub-bridge",
|
||||
actor: "agt-state-hub-bridge",
|
||||
actorType: "agt",
|
||||
principal: "agt-task-bridge",
|
||||
ttlHours: 1,
|
||||
wantEffect: api.DecisionEffectAllow,
|
||||
},
|
||||
{
|
||||
name: "state hub bridge IAM subject allow",
|
||||
subjectID: "iam:agt-state-hub-bridge",
|
||||
actor: "agt-state-hub-bridge",
|
||||
actorType: "agt",
|
||||
principal: "agt-task-bridge",
|
||||
ttlHours: 1,
|
||||
wantEffect: api.DecisionEffectAllow,
|
||||
},
|
||||
{
|
||||
name: "codex interhub bootstrap agent allow",
|
||||
subjectID: "agt-codex-interhub-bootstrap",
|
||||
actor: "agt-codex-interhub-bootstrap",
|
||||
actorType: "agt",
|
||||
principal: "agt-interhub-bootstrap",
|
||||
ttlHours: 1,
|
||||
wantEffect: api.DecisionEffectAllow,
|
||||
},
|
||||
{
|
||||
name: "admin actor allow",
|
||||
subjectID: "adm-example",
|
||||
actor: "adm-example",
|
||||
actorType: "adm",
|
||||
principal: "adm-full",
|
||||
ttlHours: 4,
|
||||
wantEffect: api.DecisionEffectAllow,
|
||||
},
|
||||
{
|
||||
name: "automation actor allow",
|
||||
subjectID: "atm-backup-daily",
|
||||
actor: "atm-backup-daily",
|
||||
actorType: "atm",
|
||||
principal: "atm-backup-daily",
|
||||
ttlHours: 1,
|
||||
wantEffect: api.DecisionEffectAllow,
|
||||
},
|
||||
{
|
||||
name: "ttl above production max denies",
|
||||
subjectID: "agt-state-hub-bridge",
|
||||
actor: "agt-state-hub-bridge",
|
||||
actorType: "agt",
|
||||
principal: "agt-task-bridge",
|
||||
ttlHours: 999,
|
||||
wantEffect: api.DecisionEffectDeny,
|
||||
wantReason: "ttl_out_of_bounds",
|
||||
},
|
||||
{
|
||||
name: "unregistered production actor denies",
|
||||
subjectID: "agt-missing",
|
||||
actor: "agt-missing",
|
||||
actorType: "agt",
|
||||
principal: "agt-missing",
|
||||
ttlHours: 1,
|
||||
wantEffect: api.DecisionEffectDeny,
|
||||
wantReason: "unknown_actor_resource",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decision, err := engine.Check(context.Background(), opsWardenProductionSignRequest(tt.subjectID, tt.actor, tt.actorType, tt.principal, tt.ttlHours))
|
||||
if err != nil {
|
||||
t.Fatalf("Check: %v", err)
|
||||
}
|
||||
if decision.Effect != tt.wantEffect {
|
||||
t.Fatalf("decision.Effect = %q; want %q; decision: %+v", decision.Effect, tt.wantEffect, decision)
|
||||
}
|
||||
if tt.wantReason != "" && decision.Reason != tt.wantReason {
|
||||
t.Fatalf("decision.Reason = %q; want %q; decision: %+v", decision.Reason, tt.wantReason, decision)
|
||||
}
|
||||
if tt.wantEffect == api.DecisionEffectAllow && decision.ID == "" {
|
||||
t.Fatal("allow decision ID is empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunValidateAccessDescriptor(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{"validate", "--kind", "access-descriptor", "--file", examplePath("access_descriptor.yaml")}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), `"status": "valid"`) {
|
||||
t.Fatalf("stdout = %s; want valid status", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func examplePath(name string) string {
|
||||
return filepath.Join("..", "..", "examples", "caring", name)
|
||||
}
|
||||
|
||||
func opsPath(name string) string {
|
||||
return filepath.Join("..", "..", "examples", "ops-warden", name)
|
||||
}
|
||||
|
||||
func opsWardenProductionSignRequest(subjectID, actor, actorType, principal string, ttlHours float64) api.CheckRequest {
|
||||
return api.CheckRequest{
|
||||
ID: "check:ops-warden-production-" + actor,
|
||||
Tenant: "tenant:platform",
|
||||
Subject: api.SubjectRef{
|
||||
ID: subjectID,
|
||||
Type: api.SubjectType(actorType),
|
||||
},
|
||||
Action: "sign",
|
||||
Resource: api.ResourceRef{
|
||||
ID: "ssh-cert:actor/" + actor,
|
||||
Type: "ssh-certificate",
|
||||
System: "ops-warden",
|
||||
},
|
||||
Context: map[string]any{
|
||||
"principals": []string{principal},
|
||||
"actor_type": actorType,
|
||||
"ttl_hours": ttlHours,
|
||||
"pubkey_fingerprint": "SHA256:example-production-fingerprint",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func postCheck(t *testing.T, url, path string) api.DecisionEnvelope {
|
||||
t.Helper()
|
||||
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("POST %s: %v", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("POST %s status = %d; want 200", path, resp.StatusCode)
|
||||
}
|
||||
var decision api.DecisionEnvelope
|
||||
if err := json.NewDecoder(resp.Body).Decode(&decision); err != nil {
|
||||
t.Fatalf("decode %s response: %v", path, err)
|
||||
}
|
||||
return decision
|
||||
}
|
||||
83
docs/adr/0001-implementation-language-and-skeleton.md
Normal file
83
docs/adr/0001-implementation-language-and-skeleton.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# ADR-0001: Implementation Language and Repo Skeleton
|
||||
|
||||
Date: 2026-05-15
|
||||
Status: Accepted
|
||||
Deciders: Bernd, with assessment from Claude (Opus 4.7)
|
||||
Supersedes: —
|
||||
|
||||
## Context
|
||||
|
||||
flex-auth is a policy-as-code authorization registry and control plane. It
|
||||
must run as a CLI and, later, a service. Its peers in the NetKingdom
|
||||
ecosystem are written in a mix of languages: `key-cape` is Go, `ops-bridge`
|
||||
and `ops-warden` are Python, the State Hub itself is Python. There is a
|
||||
recorded State Hub decision noting that Go was the right call for key-cape
|
||||
because of orchestration-heavy HTTP adapter code, fast iteration, and
|
||||
clean domain boundaries.
|
||||
|
||||
flex-auth shares the relevant traits with key-cape: HTTP/gRPC adapters to
|
||||
multiple PDPs and directory backends, latency-sensitive check paths, and
|
||||
a need to ship a single static binary for local-development ergonomics.
|
||||
|
||||
## Decision
|
||||
|
||||
- **Language: Go.**
|
||||
- **Module path: `github.com/netkingdom/flex-auth`** (placeholder; adjust
|
||||
if the repo moves under a different GitHub org during publication).
|
||||
- **Minimum Go version: matching key-cape at time of skeleton landing.**
|
||||
- **Repo layout:**
|
||||
|
||||
```text
|
||||
cmd/flex-auth/ CLI entrypoint
|
||||
cmd/flex-authd/ service entrypoint (added when the service layer lands)
|
||||
internal/registry/ resource / subject / relationship store
|
||||
internal/policy/ policy package model, Rego evaluation, fixtures
|
||||
internal/decision/ check, batch_check, list_allowed, explain, decision log
|
||||
internal/audit/ compact decision-envelope persistence
|
||||
internal/adapters/ pluggable PDP and directory adapters (later WPs)
|
||||
pkg/api/ public types and OpenAPI schemas
|
||||
schemas/ JSON Schema for manifests and envelopes
|
||||
examples/ runnable example manifests, policies, fixtures
|
||||
docs/adr/ this ADR series
|
||||
```
|
||||
|
||||
- **Build, lint, test:** `Makefile` targets `build`, `test`, `lint`,
|
||||
`tidy`, `sbom`. Linting via `golangci-lint`. Tests via the standard
|
||||
`go test ./...` plus contract fixtures.
|
||||
- **SBOM:** generate on each release tag and on `make sbom`; register via
|
||||
the State Hub `ingest_sbom_tool` so `last_sbom_at` stops being `null`.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Aligns with the only language decision in the NetKingdom ecosystem that
|
||||
has already been validated in production (KeyCape v0.1).
|
||||
- Single static binary makes the standalone-first mode trivial to ship
|
||||
for local development across NetKingdom repos.
|
||||
- Strong concurrency primitives suit batch-check and list-allowed paths.
|
||||
- Excellent OPA tooling for Go (`open-policy-agent/opa/rego`) means the
|
||||
Rego evaluator chosen in ADR-0002 has first-class library support.
|
||||
- Topaz (the target alignment from ADR-0003) is Go-native — adapter work
|
||||
in FLEX-WP-0004 stays in the same language.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New flex-auth contributors need Go in their toolchain. Python is still
|
||||
used elsewhere in the ecosystem; cross-repo work that hits the State
|
||||
Hub or ops-bridge must accept the language switch.
|
||||
- The Go decision is reversible while the repo is empty. Once `cmd/` and
|
||||
`internal/` have been populated by FLEX-WP-0005 T01, reversal becomes
|
||||
expensive — flag any reservations during the skeleton task, not later.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Database choice (SQLite vs Postgres vs file-backed) is settled in
|
||||
FLEX-WP-0002 T02 and recorded in a later ADR.
|
||||
- Service framework (net/http vs Connect vs gRPC) is deferred to the
|
||||
service-skeleton task in FLEX-WP-0002 T07.
|
||||
|
||||
## Related
|
||||
|
||||
- ADR-0002: Rego-in-Markdown policy format.
|
||||
- ADR-0003: Topaz-aligned MVP.
|
||||
- State Hub recorded decision: "Implementation language for KeyCape: Go"
|
||||
(resolved 2026-03-25).
|
||||
184
docs/adr/0002-rego-in-markdown-policy-format.md
Normal file
184
docs/adr/0002-rego-in-markdown-policy-format.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# ADR-0002: Rego-in-Markdown Policy Package Format
|
||||
|
||||
Date: 2026-05-15
|
||||
Status: Accepted
|
||||
Deciders: Bernd, with assessment from Claude (Opus 4.7)
|
||||
Supersedes: the "simple declarative rule format" placeholder in
|
||||
FLEX-WP-0002 P2.3.
|
||||
|
||||
## Context
|
||||
|
||||
flex-auth's product surface is policy-as-code with reviewable, testable,
|
||||
versioned packages and an `explain(decision_id)` API. Three candidate
|
||||
package formats were on the table:
|
||||
|
||||
1. **Bespoke YAML rules now, port to Rego later.** Lowest immediate
|
||||
complexity, but every adapter spike (Topaz, OPA, OPAL) would need a
|
||||
translator, and the "thin-wrapper-around-Topaz" trap (see assessment)
|
||||
gets worse the longer the bespoke layer exists.
|
||||
2. **Rego from day one.** Standard, battle-tested policy language with
|
||||
first-class Go library support (`open-policy-agent/opa/rego`). Reuse
|
||||
for Topaz, OPA, OPAL, Permit, and most ReBAC-adjacent ecosystems.
|
||||
3. **Cedar.** Strong typing and analyzability; smaller ecosystem; less
|
||||
alignment with Topaz, which is the MVP backend target in ADR-0003.
|
||||
|
||||
The product also values **intent + validation co-location**. Markdown is
|
||||
already the lingua franca of the NetKingdom knowledge plane (Markitect is
|
||||
the first consumer), and reviewable policy benefits more from prose
|
||||
context than YAML or pure `.rego` files can provide on their own.
|
||||
|
||||
## Decision
|
||||
|
||||
A **policy package is a Markdown document** with:
|
||||
|
||||
1. YAML frontmatter describing package metadata (id, version, namespace,
|
||||
action vocabulary scope, owner, status, activation, fixtures source).
|
||||
2. Prose sections (free-form) capturing **intent**: what the policy
|
||||
protects, why, what the failure modes look like, what break-glass
|
||||
semantics apply.
|
||||
3. Fenced `rego` blocks containing the rules.
|
||||
4. Fenced `rego` blocks tagged `test` containing OPA-compatible tests.
|
||||
5. Fenced `yaml` blocks tagged `fixture` containing decision fixtures
|
||||
(input/expected-decision pairs) that the loader can also evaluate
|
||||
against the Rego module.
|
||||
|
||||
The loader extracts and concatenates the `rego` blocks into one OPA
|
||||
module per package (using the `package` declaration in frontmatter), the
|
||||
`test` blocks into a sibling test module, and the `fixture` blocks into
|
||||
a fixtures table. Validation runs `opa parse`, `opa test`, and the
|
||||
fixtures evaluator before a package can be marked `valid`.
|
||||
|
||||
### Minimal example
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: markitect.documents.internal-read
|
||||
version: 0.1.0
|
||||
namespace: markitect:document
|
||||
package: flexauth.markitect.documents
|
||||
actions: [read, query, search]
|
||||
owner: team:platform-architecture
|
||||
status: draft
|
||||
fixtures:
|
||||
- examples/markitect/internal_doc_allow.yaml
|
||||
- examples/markitect/internal_doc_deny.yaml
|
||||
---
|
||||
|
||||
# Internal Document Read
|
||||
|
||||
This package gates `read`, `query`, and `search` on documents labelled
|
||||
`internal`. A subject must be in the `reader` group of the document's
|
||||
owning team, or hold the `steward` role on the document's repository.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- Stale group membership: resolver freshness must be within 15 minutes
|
||||
or the decision becomes `audit_only` with a `stale_directory` reason.
|
||||
- Break-glass: an `emergency_principal` claim with a logged reason
|
||||
produces `allow` with an obligation to record an export receipt.
|
||||
|
||||
## Rules
|
||||
|
||||
```rego
|
||||
package flexauth.markitect.documents
|
||||
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
default decision := {"effect": "deny", "reason": "no_matching_rule"}
|
||||
|
||||
decision := {"effect": "allow", "reason": "reader_group"} if {
|
||||
input.action in {"read", "query", "search"}
|
||||
input.resource.labels[_] == "internal"
|
||||
some g in input.subject.groups
|
||||
g == sprintf("reader:%s", [input.resource.owner_team])
|
||||
}
|
||||
|
||||
decision := {"effect": "allow", "reason": "steward_role"} if {
|
||||
input.action in {"read", "query", "search"}
|
||||
"steward" in input.subject.roles_on[input.resource.repository]
|
||||
}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```rego test
|
||||
package flexauth.markitect.documents_test
|
||||
|
||||
import data.flexauth.markitect.documents
|
||||
|
||||
test_reader_group_allows_read if {
|
||||
documents.decision.effect == "allow" with input as {
|
||||
"action": "read",
|
||||
"subject": {"groups": ["reader:platform-architecture"]},
|
||||
"resource": {"labels": ["internal"], "owner_team": "platform-architecture"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
```yaml fixture
|
||||
- name: reader group allow
|
||||
input:
|
||||
action: read
|
||||
subject: {groups: ["reader:platform-architecture"]}
|
||||
resource: {labels: ["internal"], owner_team: "platform-architecture"}
|
||||
expect: {effect: allow, reason: reader_group}
|
||||
- name: no group deny
|
||||
input:
|
||||
action: read
|
||||
subject: {groups: []}
|
||||
resource: {labels: ["internal"], owner_team: "platform-architecture"}
|
||||
expect: {effect: deny, reason: no_matching_rule}
|
||||
```
|
||||
```
|
||||
|
||||
(Fence backticks above are zero-width-spaced for documentation. The real
|
||||
loader expects normal triple backticks.)
|
||||
|
||||
## Rationale
|
||||
|
||||
- **Intent and validation co-located.** A reviewer reading the package
|
||||
sees *why* alongside *what*. ADR text and policy code don't drift.
|
||||
- **Standard evaluator.** OPA's `rego` library is mature, well-tested,
|
||||
and the dominant Rego implementation. flex-auth gets correctness and
|
||||
performance work for free.
|
||||
- **Topaz alignment from day one.** ADR-0003 commits to shaping the
|
||||
standalone core so the Topaz adapter (FLEX-WP-0004 T01) is a small
|
||||
step. Rego is Topaz's native policy language.
|
||||
- **Markitect synergy.** The first consumer is a Markdown knowledge
|
||||
system. Policy packages and the documents they protect share one
|
||||
authoring substrate; Markitect renders policy packages natively;
|
||||
policy package versions sit comfortably in the same review process as
|
||||
any other document.
|
||||
- **Tooling reuse.** `opa fmt`, `opa test`, `opa eval`, and the broader
|
||||
OPA tool ecosystem just work on extracted modules.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The loader must implement Markdown extraction. Off-the-shelf Goldmark
|
||||
+ a small block walker covers it; tests must lock the fence syntax.
|
||||
- Authors must learn Rego. This is the universally accepted cost of
|
||||
Rego adoption. The literate format lowers the cliff by surrounding
|
||||
the language with intent prose.
|
||||
- Some pure-data fixtures still live in `examples/` and are referenced
|
||||
from the frontmatter — keeps large fixture files out of the policy
|
||||
document body.
|
||||
- An ADR amendment will be needed if Cedar or a typed policy language
|
||||
enters the picture as a *peer* (not adapter). The literate Markdown
|
||||
envelope can accommodate other fenced languages, but `rego` is the
|
||||
baseline.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Whether activation, rollout, and rollback semantics live in
|
||||
frontmatter or in a sibling activation manifest — settled in
|
||||
FLEX-WP-0002 P2.3.
|
||||
- The exact extraction library — implementation detail of P2.3.
|
||||
|
||||
## Related
|
||||
|
||||
- ADR-0001: Go implementation (provides the OPA library path).
|
||||
- ADR-0003: Topaz-aligned MVP (provides the backend rationale).
|
||||
- FLEX-WP-0002 P2.3 (policy package loader and validator).
|
||||
87
docs/adr/0003-topaz-aligned-mvp.md
Normal file
87
docs/adr/0003-topaz-aligned-mvp.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# ADR-0003: Topaz-Aligned MVP
|
||||
|
||||
Date: 2026-05-15
|
||||
Status: Accepted
|
||||
Deciders: Bernd, with assessment from Claude (Opus 4.7)
|
||||
Supersedes: implicit "standalone first, evaluate Topaz later" sequencing
|
||||
in the original FLEX-WP-0004 P4.1.
|
||||
|
||||
## Context
|
||||
|
||||
The pre-implementation assessment names a primary threat: the
|
||||
"thin-wrapper-around-Topaz" trap. Topaz already combines OPA/Rego
|
||||
evaluation with a local directory inspired by Zanzibar (users, groups,
|
||||
resources, relations) and a local deployment story. If flex-auth's
|
||||
standalone core reimplements 60% of Topaz badly and then adapts to Topaz
|
||||
anyway, the abstraction earns no product value.
|
||||
|
||||
ADR-0002 commits to Rego from day one. That removes the largest source
|
||||
of friction between the standalone evaluator and Topaz, because the
|
||||
policy language is identical.
|
||||
|
||||
The remaining question is sequencing. The original workplans placed the
|
||||
Topaz evaluation at FLEX-WP-0004 P4.1, *after* the standalone core
|
||||
shapes its registry, check API, and decision log. That sequencing is
|
||||
inverted: by the time the spike runs, the core has already made shape
|
||||
decisions that the Topaz adapter will then have to translate.
|
||||
|
||||
## Decision
|
||||
|
||||
- **flex-auth's actual product surface is registry + audit + explain +
|
||||
multi-consumer + literate policy packages.** The PDP is an
|
||||
implementation detail behind a stable contract.
|
||||
- **Shape the standalone core to be Rego/Topaz-aligned from day one.**
|
||||
Concretely:
|
||||
- The standalone evaluator embeds the OPA Rego library and evaluates
|
||||
the same Rego modules a Topaz deployment would.
|
||||
- The registry's resource/subject/relation model uses vocabulary that
|
||||
maps trivially onto Topaz directory objects and relations.
|
||||
- The decision envelope carries the same provenance fields whether
|
||||
evaluation is local or delegated.
|
||||
- **The Topaz evaluation spike moves earlier.** It runs in
|
||||
FLEX-WP-0005 (Foundations), not FLEX-WP-0004, so its findings inform
|
||||
the registry, policy loader, and check API before they are written.
|
||||
- **FLEX-WP-0004 keeps Topaz only as one delegated-mode adapter**
|
||||
alongside OpenFGA, SpiceDB, OPA-as-remote, Cedar, and Keycloak
|
||||
Authorization Services. Topaz is no longer the sole evaluation
|
||||
question of FLEX-WP-0004.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Aligning vocabulary up front is cheap; aligning it retroactively is
|
||||
not.
|
||||
- The standalone mode remains genuinely useful (zero external services,
|
||||
single binary, fixtures-first development) while staying Topaz-shaped
|
||||
internally.
|
||||
- Topaz remains an opinionated, single-vendor product. The product
|
||||
surface (registry, audit, explain, multi-consumer) is where flex-auth
|
||||
competes; the evaluation engine is where flex-auth integrates.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The Topaz spike (FLEX-WP-0005 T04) becomes a hard prerequisite for
|
||||
FLEX-WP-0002 P2.1 schemas. Its output is a mapping document showing
|
||||
how flex-auth resources/relations correspond to Topaz directory
|
||||
objects/relations, plus a recommendation on whether to embed Topaz's
|
||||
directory contracts directly or restate them.
|
||||
- FLEX-WP-0004 is slimmed (Topaz evaluation moves out, leaving the
|
||||
Topaz adapter implementation in T01 and the relationship/rule/
|
||||
Keycloak/directory adapter work in T02–T06).
|
||||
- Keycloak Authorization Services remains an adapter path for
|
||||
Keycloak-centric deployments — flex-auth is the canonical PAP/PDP,
|
||||
Keycloak AuthZ is one delegated PDP option.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- The exact wire protocol of the Topaz adapter (gRPC vs HTTP vs library
|
||||
embed) is decided in FLEX-WP-0004 T01.
|
||||
- The directory-store implementation in standalone mode (SQLite vs
|
||||
file-backed vs in-memory) is decided in FLEX-WP-0002 T02.
|
||||
|
||||
## Related
|
||||
|
||||
- ADR-0001: Go implementation (matches Topaz's language).
|
||||
- ADR-0002: Rego-in-Markdown policy format (matches Topaz's policy
|
||||
language).
|
||||
- FLEX-WP-0005 T04: Topaz mapping spike.
|
||||
- FLEX-WP-0004: Delegated PDP and directory adapters.
|
||||
203
docs/caring-architecture-blueprint.md
Normal file
203
docs/caring-architecture-blueprint.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# CARING Architecture Blueprint For Flex-Auth
|
||||
|
||||
Date: 2026-05-17
|
||||
Status: Draft architecture blueprint
|
||||
Source standard: CARING 0.4.0-RC2 (`/home/worsch/helix-forge/wiki/CaringStandardRc2.md`)
|
||||
|
||||
## Purpose
|
||||
|
||||
This blueprint describes how flex-auth can serve as the practical,
|
||||
efficient reference implementation of the CARING standard while keeping
|
||||
the core authorization path small enough to build and operate.
|
||||
|
||||
CARING remains the semantic standard. Flex-auth implements the subset
|
||||
needed to make CARING descriptors, policy conformance, decisions,
|
||||
explanations, and audit events executable.
|
||||
|
||||
## Architecture Position
|
||||
|
||||
CARING answers:
|
||||
|
||||
```text
|
||||
What access semantics should a system expose, validate, explain, and audit?
|
||||
```
|
||||
|
||||
Flex-auth answers:
|
||||
|
||||
```text
|
||||
Given a subject, action, resource, context, policy package, and registry
|
||||
state, what decision should protected systems enforce?
|
||||
```
|
||||
|
||||
The reference implementation relationship is:
|
||||
|
||||
```text
|
||||
CARING standard
|
||||
-> flex-auth CARING profile schemas
|
||||
-> policy packages and conformance checks
|
||||
-> check/batch/list/explain APIs
|
||||
-> decision and exposure-event audit records
|
||||
```
|
||||
|
||||
## Minimal CARING Profile
|
||||
|
||||
The first implementation should pin a `caring_profile` version and define
|
||||
the following canonical fields in flex-auth API and schema artifacts:
|
||||
|
||||
```text
|
||||
subject_type
|
||||
organization_relation
|
||||
canonical_role
|
||||
scope
|
||||
plane
|
||||
capabilities
|
||||
exposure_mode
|
||||
conditions
|
||||
lifecycle_state
|
||||
restrictions
|
||||
exposure_event
|
||||
derived_capabilities
|
||||
access_path
|
||||
```
|
||||
|
||||
The core does not need to implement every CARING benchmark or native-system
|
||||
mapping immediately. It only needs the descriptor shape, enums, validation,
|
||||
and a place in decisions and audit records.
|
||||
|
||||
## Core Data Types
|
||||
|
||||
`pkg/api` should expose:
|
||||
|
||||
```text
|
||||
CaringProfile
|
||||
CaringAccessDescriptor
|
||||
CaringConformanceFinding
|
||||
CaringExposureEvent
|
||||
CaringRestriction
|
||||
CaringDerivedCapability
|
||||
```
|
||||
|
||||
These types should be referenced by:
|
||||
|
||||
```text
|
||||
SubjectManifest
|
||||
ResourceManifest
|
||||
RelationshipFact
|
||||
PolicyPackageMetadata
|
||||
CheckRequest
|
||||
DecisionEnvelope
|
||||
AuditEvent
|
||||
```
|
||||
|
||||
The `ResourceManifest` remains consumer-owned and lightweight. CARING
|
||||
classification should be optional on resources at first, with policy
|
||||
packages allowed to require specific fields for a protected system.
|
||||
|
||||
## Decision Pipeline
|
||||
|
||||
The efficient runtime path is:
|
||||
|
||||
```text
|
||||
1. Normalize identity claims into a Subject.
|
||||
2. Load registry facts for resource, relationships, tenant, and groups.
|
||||
3. Build a CheckRequest with CARING context.
|
||||
4. Evaluate restrictions first.
|
||||
5. Evaluate scoped policy package rules.
|
||||
6. Produce a DecisionEnvelope.
|
||||
7. Attach CARING conformance findings and provenance.
|
||||
8. Persist audit/exposure-event records when required.
|
||||
```
|
||||
|
||||
CARING conformance should not block the fast allow/deny path unless a
|
||||
policy marks a finding as enforcement-grade. Most early findings should
|
||||
be diagnostics, warnings, or audit requirements.
|
||||
|
||||
## Conformance Model
|
||||
|
||||
Flex-auth should support two CARING modes:
|
||||
|
||||
```text
|
||||
descriptive
|
||||
Map existing local roles, policy objects, and grants into CARING
|
||||
descriptors. Report ambiguity, bundling, missing scope, missing plane,
|
||||
induced access, and exposure gaps.
|
||||
|
||||
prescriptive
|
||||
Validate new policy packages and manifests against a CARING profile.
|
||||
Fail on required fields, restriction precedence, tenant-boundary
|
||||
violations, and explicit policy-plane or secret-plane violations.
|
||||
```
|
||||
|
||||
The conformance output should use stable severities:
|
||||
|
||||
```text
|
||||
info
|
||||
warning
|
||||
violation
|
||||
blocked
|
||||
```
|
||||
|
||||
## Policy Package Shape
|
||||
|
||||
Rego-in-Markdown policy packages should carry CARING frontmatter:
|
||||
|
||||
```yaml
|
||||
caring:
|
||||
profile: caring-0.4.0-rc2
|
||||
canonical_roles: [Doer]
|
||||
organization_relations: [Customer]
|
||||
planes: [Data]
|
||||
capabilities: [View]
|
||||
exposure_modes: [Masked, Plaintext]
|
||||
conditions: [PurposeBound, Logged]
|
||||
restrictions: [ExportBlocked, CrossTenantBlocked]
|
||||
```
|
||||
|
||||
The policy evaluator should provide this metadata to Rego and copy the
|
||||
matched CARING descriptor into the decision envelope.
|
||||
|
||||
## Reference Implementation Boundaries
|
||||
|
||||
Flex-auth should implement:
|
||||
|
||||
```text
|
||||
CARING descriptor schemas
|
||||
CARING-aware policy package validation
|
||||
CARING-aware decision envelopes
|
||||
CARING explain output
|
||||
CARING exposure-event audit records
|
||||
CARING conformance fixtures
|
||||
```
|
||||
|
||||
Flex-auth should not own:
|
||||
|
||||
```text
|
||||
The CARING standard text
|
||||
The NetKingdom IAM Profile
|
||||
Identity-provider role issuance
|
||||
Consumer-specific product semantics
|
||||
Full benchmark mappings for every native access system
|
||||
```
|
||||
|
||||
## Implementation Slices
|
||||
|
||||
1. Pin the CARING profile and descriptors in `FLEX-WP-0002 P2.1`.
|
||||
2. Add registry fields and validation in `FLEX-WP-0002 P2.2`.
|
||||
3. Add policy frontmatter and diagnostics in `FLEX-WP-0002 P2.3`.
|
||||
4. Attach CARING metadata to `check` and `batch_check` in `FLEX-WP-0002 P2.4`.
|
||||
5. Use CARING language in `list_allowed` and `explain` in `FLEX-WP-0002 P2.5`.
|
||||
6. Persist CARING exposure events in `FLEX-WP-0002 P2.6`.
|
||||
7. Map Markitect fixtures as the first consumer benchmark in `FLEX-WP-0003`.
|
||||
8. Require delegated adapters in `FLEX-WP-0004` to preserve CARING envelope
|
||||
fields even when backend-native semantics differ.
|
||||
|
||||
## Open Design Choices
|
||||
|
||||
The first implementation should decide:
|
||||
|
||||
```text
|
||||
Whether CARING enum values are strict or extension-friendly.
|
||||
Whether conformance findings are always non-blocking by default.
|
||||
How much derived-capability analysis belongs in core versus adapters.
|
||||
How to version profile changes after CARING leaves RC status.
|
||||
```
|
||||
133
docs/delegated-mode-operations.md
Normal file
133
docs/delegated-mode-operations.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Delegated Mode Operations
|
||||
|
||||
Status: implemented for FLEX-WP-0004 P4.6.
|
||||
|
||||
## Purpose
|
||||
|
||||
Delegated mode lets flex-auth coordinate external authorization systems
|
||||
without changing the protected-system-facing API. Protected systems keep
|
||||
using flex-auth check, batch, list, explanation, and audit contracts.
|
||||
Topaz, relationship PDPs, rule PDPs, Keycloak Authorization Services,
|
||||
and directory resolvers stay behind adapter boundaries.
|
||||
|
||||
## Deployment Order
|
||||
|
||||
1. Load protected-system manifests and resource manifests into the
|
||||
flex-auth registry.
|
||||
2. Load subject manifests or configure directory resolvers.
|
||||
3. Import resources and relationships into the delegated backend.
|
||||
4. Import policy artifacts or backend-native policy mirrors.
|
||||
5. Run adapter health checks.
|
||||
6. Start serving delegated checks only after the backend reports healthy
|
||||
and the latest import has a recorded consistency token.
|
||||
|
||||
For Topaz, the local reference topology is `examples/topaz`. For
|
||||
Keycloak, register resources from flex-auth manifests before enabling
|
||||
UMA permission checks. For OpenFGA/SpiceDB-style systems, import tuples
|
||||
from the canonical registry snapshot. For OPA/Cedar-style systems,
|
||||
import policy artifacts from validated flex-auth policy packages.
|
||||
|
||||
## Health Checks
|
||||
|
||||
Each delegated adapter exposes a health boundary. Runtime health should
|
||||
be tracked separately for:
|
||||
|
||||
- backend reachability
|
||||
- policy import freshness
|
||||
- directory or tuple import freshness
|
||||
- resolver freshness
|
||||
- last successful decision time
|
||||
|
||||
Health failures should not silently fall back to stale allow decisions.
|
||||
They should produce observable diagnostics and deny fail-closed unless a
|
||||
deployment has explicitly accepted a narrower fail-open mode for a
|
||||
non-sensitive action.
|
||||
|
||||
## Fail-Closed Default
|
||||
|
||||
The default behavior is fail-closed:
|
||||
|
||||
| Condition | Decision effect | Typical reason |
|
||||
| --- | --- | --- |
|
||||
| backend unavailable | deny | `topaz_unavailable`, `relationship_backend_unavailable`, `rule_backend_unavailable`, `keycloak_unavailable` |
|
||||
| stale directory or tuple data | deny | `topaz_directory_stale`, `relationship_data_stale` |
|
||||
| stale policy | deny | `rule_policy_stale`, `keycloak_policy_stale` |
|
||||
| partial backend result | deny | `*_partial_result` |
|
||||
| request cannot be translated | deny | `*_request_incomplete` |
|
||||
|
||||
Fail-open is only acceptable for explicitly classified low-risk
|
||||
capabilities and must be recorded as a policy decision, not an adapter
|
||||
default. Data, identity, policy, secret, audit, and commercial planes
|
||||
should remain fail-closed.
|
||||
|
||||
## Caching
|
||||
|
||||
Recommended cache layers:
|
||||
|
||||
- resource manifest snapshot cache
|
||||
- subject/group resolver cache
|
||||
- delegated backend import token cache
|
||||
- decision-result cache for idempotent low-risk checks
|
||||
|
||||
Cache entries must record source, retrieval time, max age, and expiry.
|
||||
Directory resolver results already expose `Freshness`; tuple and
|
||||
Topaz/Keycloak imports expose consistency metadata through the decision
|
||||
provenance fields. Cache hits should still include provenance so audit
|
||||
readers can tell whether evidence came from a fresh backend call or a
|
||||
bounded cache.
|
||||
|
||||
## Consistency
|
||||
|
||||
flex-auth uses `DecisionEnvelope.provenance.directory_etag` as the
|
||||
adapter-neutral consistency token field:
|
||||
|
||||
- Topaz: relation etag when available
|
||||
- OpenFGA/SpiceDB-style backends: model id, zedtoken, or tuple-store
|
||||
freshness token
|
||||
- directory resolvers: freshness metadata attached to subject metadata
|
||||
- Keycloak/rule PDPs: policy version in `provenance.policy_version`
|
||||
|
||||
Adapters should deny or mark stale when a request demands a newer token
|
||||
than the backend can satisfy. Read-your-writes paths should import
|
||||
registry data, record the returned token, and require that token for the
|
||||
first protected-system checks after import.
|
||||
|
||||
## Audit Behavior
|
||||
|
||||
Delegated decisions must log the same canonical envelope as standalone
|
||||
decisions:
|
||||
|
||||
- subject and resource references
|
||||
- effect, reason, matched rule, matched policy version
|
||||
- obligations
|
||||
- diagnostics
|
||||
- evaluator and mode
|
||||
- consistency token
|
||||
- CARING descriptor, restrictions, exposure modes, derived
|
||||
capabilities, exposure events, and conformance findings
|
||||
|
||||
Backend-native traces can appear in diagnostics, but they must not
|
||||
replace the canonical fields. Explanations should be generated from the
|
||||
flex-auth envelope so switching backends does not change protected-system
|
||||
or auditor vocabulary.
|
||||
|
||||
## Adapter Notes
|
||||
|
||||
- Topaz operations: `docs/topaz-adapter-operations.md`
|
||||
- Relationship PDPs: `docs/relationship-pdp-adapter-boundary.md`
|
||||
- Rule PDPs: `docs/rule-pdp-adapter-boundary.md`
|
||||
- Keycloak Authorization Services:
|
||||
`docs/keycloak-authz-adapter-path.md`
|
||||
- Directory resolvers: `docs/directory-group-resolver-adapters.md`
|
||||
|
||||
## Operational Checklist
|
||||
|
||||
- Backend configured and reachable.
|
||||
- Policy/resource/tuple import completed.
|
||||
- Latest import token recorded.
|
||||
- Directory resolver freshness thresholds configured.
|
||||
- Fail-closed behavior tested for unavailable, stale, partial, and
|
||||
invalid-request cases.
|
||||
- Decision log captures delegated provenance and CARING metadata.
|
||||
- Rollback path can switch a protected system back to standalone mode or
|
||||
to another delegated adapter without changing the protected-system API.
|
||||
65
docs/directory-group-resolver-adapters.md
Normal file
65
docs/directory-group-resolver-adapters.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Directory Group Resolver Adapters
|
||||
|
||||
Status: implemented for FLEX-WP-0004 P4.5.
|
||||
|
||||
## Role
|
||||
|
||||
Directory resolvers enrich flex-auth subjects with group and role
|
||||
evidence from external identity systems. They do not decide access by
|
||||
themselves. They feed the standalone, relationship, rule, Topaz, or
|
||||
Keycloak decision paths with fresh subject metadata and provenance.
|
||||
|
||||
## Resolver Sources
|
||||
|
||||
Implemented resolver boundaries:
|
||||
|
||||
- Microsoft Graph group overage (`GraphResolver`)
|
||||
- SCIM provisioning (`SCIMResolver`)
|
||||
- LDAP/Active Directory (`LDAPResolver`)
|
||||
- Keycloak admin API (`KeycloakResolver`)
|
||||
|
||||
Each resolver returns `ResolveResult` with normalized groups, roles,
|
||||
freshness, overage, and metadata.
|
||||
|
||||
## Freshness
|
||||
|
||||
Every result carries:
|
||||
|
||||
- source
|
||||
- retrieval time
|
||||
- max age
|
||||
- expiry time
|
||||
- stale flag
|
||||
|
||||
Decision adapters can use this metadata to deny stale directory evidence
|
||||
or to include freshness diagnostics in the decision envelope.
|
||||
|
||||
## Overage
|
||||
|
||||
Graph tokens can omit groups and instead emit overage indicators such
|
||||
as `_claim_names.groups` or `hasgroups=true`. The Graph resolver records
|
||||
that condition in `OverageMetadata` so downstream policy can distinguish
|
||||
"no groups" from "groups omitted, lookup required".
|
||||
|
||||
## CARING Provenance
|
||||
|
||||
Each `GroupGrant` and `RoleGrant` identifies:
|
||||
|
||||
- source provider
|
||||
- originating claim name
|
||||
- organization relation
|
||||
- subject type
|
||||
- optional CARING descriptor
|
||||
|
||||
This makes it possible to explain that a canonical role or group came
|
||||
from Graph, SCIM, LDAP/AD, or Keycloak rather than from an opaque token
|
||||
claim. The source remains inspectable in CARING conformance reviews.
|
||||
|
||||
## Subject Enrichment
|
||||
|
||||
`MergeResults` deduplicates groups and roles across providers while
|
||||
preserving freshness, overage, and descriptors. `ApplyToSubject` returns
|
||||
a subject with resolved groups/roles and directory metadata attached.
|
||||
|
||||
The enriched subject can then flow through any flex-auth decision path
|
||||
without changing protected-system request or decision contracts.
|
||||
181
docs/iam-profile-consumption.md
Normal file
181
docs/iam-profile-consumption.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# NetKingdom IAM Profile — flex-auth Consumption Surface
|
||||
|
||||
Date: 2026-05-22
|
||||
Status: Aligned with NetKingdom IAM Profile v0.2; binds the input contract
|
||||
for the standalone evaluator (FLEX-WP-0002) and every PDP adapter
|
||||
(FLEX-WP-0004).
|
||||
Upstream: `~/net-kingdom/canon/standards/iam-profile_v0.2.md`.
|
||||
|
||||
## Boundary
|
||||
|
||||
The NetKingdom IAM Profile defines the OIDC contract shared across
|
||||
platform, tenant, service, and agent principals. 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
|
||||
tenant: tenant:platform | tenant:<id> # required
|
||||
principal_type: human | service | agent
|
||||
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>, ...] # required, may be empty
|
||||
assurance:
|
||||
level: aal0 | aal1 | aal2 | aal3 | break_glass
|
||||
methods: [<method>, ...]
|
||||
mfa: <bool>
|
||||
source: <identity or MFA evidence source>
|
||||
at: <unix timestamp, optional>
|
||||
acr: <oidc acr value, optional>
|
||||
amr: [<oidc amr value>, ...] # tolerated provider-native input
|
||||
agent:
|
||||
id: <agent id, optional>
|
||||
mode: autonomous | delegated
|
||||
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 v0.2 "Core 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. |
|
||||
| `tenant` | `tenant` | Required for platform/tenant boundary decisions. |
|
||||
| `principal_type` | `principal_type` | `human`, `service`, or `agent`; emergency is a role plus `assurance.level=break_glass`. |
|
||||
| `groups` | `groups` | Required, possibly empty; overage is handled by directory resolvers. |
|
||||
| `scope` or `scp` | `scopes` | At least one scope required. Empty scope is a hard fail. |
|
||||
| `roles` | `roles` | Canonical role source. At least one role required by current flex-auth policy fixtures. |
|
||||
| `assurance` | `assurance` | Required normalized evidence object with level, methods, mfa, and source. |
|
||||
| `preferred_username` | `preferred_username` | Required for `principal_type=human`. Optional for service and agent principals. |
|
||||
|
||||
## Recommended Claims
|
||||
|
||||
| Claim | flex-auth field | Use |
|
||||
| --- | --- | --- |
|
||||
| `email` | `claims.email` | Contact identity; **never** used for authorization decisions. |
|
||||
| `name` | `claims.name` | Display only. |
|
||||
| `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.** IAM Profile v0.2 makes top-level `roles`
|
||||
canonical. During migration, flex-auth may also accept Keycloak's
|
||||
`realm_access.roles` and `resource_access.<client>.roles`, but those
|
||||
are provider-native compatibility inputs.
|
||||
- **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.** IAM Profile v0.2 uses `assurance.mfa` and
|
||||
`assurance.level`. Legacy/provider-native `amr` and `acr` are
|
||||
tolerated as inputs to the normalized assurance object.
|
||||
|
||||
## Principal-Type Detection
|
||||
|
||||
IAM Profile v0.2 supplies `principal_type` directly. flex-auth uses that
|
||||
claim as normative input. Legacy fixtures may be classified 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 agent metadata is present → `agent`.
|
||||
4. Otherwise → `human`.
|
||||
|
||||
This matches Markitect's `NetKingdomIdentityClaimsAdapter._principal_type`
|
||||
as a compatibility path. New claim envelopes should not force flex-auth
|
||||
to infer principal type.
|
||||
|
||||
## 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 access is represented as a human, service, or agent
|
||||
principal with an `emergency`/`break-glass` role and
|
||||
`assurance.level: break_glass`.
|
||||
- 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/`.
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
- `roles` is canonical in IAM Profile v0.2. `realm_access.roles` and
|
||||
`resource_access.<client>.roles` remain tolerated provider-native inputs
|
||||
while Keycloak mappings are updated.
|
||||
- Workload identity may enter through a documented token-exchange path,
|
||||
but the normalized envelope still carries `principal_type: service` or
|
||||
`principal_type: agent`, `tenant`, and `assurance`.
|
||||
78
docs/keycloak-authz-adapter-path.md
Normal file
78
docs/keycloak-authz-adapter-path.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Keycloak Authorization Services Adapter Path
|
||||
|
||||
Status: implemented for FLEX-WP-0004 P4.4.
|
||||
|
||||
## Role
|
||||
|
||||
The Keycloak path is for deployments that already use Keycloak as the
|
||||
identity provider and want to evaluate some authorization decisions
|
||||
through Keycloak Authorization Services. flex-auth still remains the
|
||||
source of truth for protected-system resources, CARING descriptors, and
|
||||
decision envelopes.
|
||||
|
||||
## Mapping
|
||||
|
||||
flex-auth maps a check to Keycloak's UMA permission shape:
|
||||
|
||||
| flex-auth | Keycloak |
|
||||
| --- | --- |
|
||||
| protected system | resource server / audience |
|
||||
| resource id | resource id |
|
||||
| action | scope |
|
||||
| subject | requesting party |
|
||||
| context and CARING descriptor | claim token |
|
||||
|
||||
The adapter builds a permission as `resource_id#scope`, for example
|
||||
`document:internal-note#read`.
|
||||
|
||||
## Resource Registration
|
||||
|
||||
`ResourceRegistrationsFromManifest` converts a
|
||||
`ResourceManifest` into Keycloak resource registrations:
|
||||
|
||||
- resource id and type are preserved;
|
||||
- manifest actions become scopes;
|
||||
- path becomes the URI;
|
||||
- labels, trust zone, owner, parent, system, and type are stored as
|
||||
resource attributes.
|
||||
|
||||
Keycloak can mirror these resources, but flex-auth keeps the original
|
||||
manifest as the canonical record.
|
||||
|
||||
## Decision Wrapping
|
||||
|
||||
Keycloak allow/deny results are wrapped into the standard
|
||||
`DecisionEnvelope`:
|
||||
|
||||
- `provenance.evaluator=keycloak-authz`
|
||||
- `provenance.mode=delegated`
|
||||
- Keycloak RPT token id and permission appear in diagnostics
|
||||
- CARING descriptor and conformance findings are preserved
|
||||
|
||||
Backend-native Keycloak policy names do not replace CARING canonical
|
||||
roles in protected-system responses.
|
||||
|
||||
## Failure Behavior
|
||||
|
||||
The adapter fails closed for:
|
||||
|
||||
- Keycloak unavailable: `keycloak_unavailable`
|
||||
- stale policy state: `keycloak_policy_stale`
|
||||
- partial result: `keycloak_partial_result`
|
||||
- untranslatable request: `keycloak_request_incomplete`
|
||||
|
||||
Each failure returns a deny envelope with `diagnostics.keycloak_failure`
|
||||
and a CARING conformance finding.
|
||||
|
||||
## Boundaries
|
||||
|
||||
This path intentionally does not make Keycloak the only policy source of
|
||||
truth. flex-auth continues to own:
|
||||
|
||||
- resource manifests from protected systems;
|
||||
- CARING descriptors and conformance findings;
|
||||
- audit and explanation envelope shape;
|
||||
- adapter-neutral request/decision APIs.
|
||||
|
||||
Keycloak is a delegated evaluator and resource mirror for Keycloak-heavy
|
||||
installations, not the canonical model for the whole product.
|
||||
24
docs/markitect-action-vocabulary.md
Normal file
24
docs/markitect-action-vocabulary.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Markitect Action Vocabulary
|
||||
|
||||
This document defines the action vocabulary for Markitect as a flex-auth
|
||||
protected system. Actions are normalized before policy evaluation so Markitect
|
||||
local behavior maps cleanly to CARING capabilities and exposure modes.
|
||||
|
||||
| Action | Markitect policy-gateway meaning | CARING capabilities | CARING planes | Exposure modes | Decision effects |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `read` | Render or fetch one document/resource. | `View` | `Data` | `Metadata`, `Masked`, `Plaintext` | `allow`, `deny`, `redact` |
|
||||
| `query` | Answer over a bounded resource set. | `ViewCollection`, `Observe` | `Data` | `Metadata`, `Aggregated`, `Masked` | `allow`, `deny`, `redact` |
|
||||
| `search` | Search index or metadata across resources. | `ViewCollection`, `Observe` | `Data` | `Metadata`, `Aggregated`, `Masked` | `allow`, `deny`, `redact` |
|
||||
| `package` | Build a context package from selected resources. | `Create`, `Bind`, `ViewCollection` | `Intent`, `Data` | `Metadata`, `Masked` | `allow`, `deny`, `audit_only` |
|
||||
| `activate_context` | Activate a prepared context package for model/tool use. | `Use`, `Execute` | `Intent`, `Policy` | `Metadata`, `Masked` | `allow`, `deny`, `audit_only` |
|
||||
| `export` | Materialize or transfer content outside Markitect. | `Export` | `Data`, `Audit` | `Exportable`, `Plaintext` | `allow`, `deny`, `audit_only` |
|
||||
| `workflow_run` | Execute a workflow using Markitect resources. | `Execute`, `Operate` | `Execution`, `Data`, `Audit` | `Metadata`, `Masked`, `Plaintext` | `allow`, `deny`, `audit_only` |
|
||||
| `admin` | Configure Markitect policy, identity, or resource controls. | `Configure`, `Grant`, `Revoke`, `Audit` | `Configuration`, `Identity`, `Policy`, `Audit` | `Metadata`, `Plaintext` | `allow`, `deny`, `audit_only` |
|
||||
|
||||
`read`, `query`, and `search` never imply `Export`. Export is separate because
|
||||
it changes the exposure mode to `Exportable` and usually requires explicit
|
||||
conditions such as MFA and logging.
|
||||
|
||||
The code-level source of truth is `internal/markitect/actions.go`. The pinned
|
||||
manifest example in `examples/markitect/protected_system_manifest.yaml` mirrors
|
||||
that vocabulary as protected-system action definitions.
|
||||
119
docs/markitect-integration-flow.md
Normal file
119
docs/markitect-integration-flow.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Markitect Integration Flow
|
||||
|
||||
This document describes how Markitect should use flex-auth as its first
|
||||
protected-system consumer integration.
|
||||
|
||||
## 1. Publish Resources
|
||||
|
||||
Markitect emits a `FlexAuthResourceManifest` for each knowledge base or
|
||||
resource slice it wants flex-auth to authorize. The emitted manifest should use
|
||||
the namespace in `docs/markitect-resource-namespace.md` and include:
|
||||
|
||||
- stable resource ids such as `document:internal-note`
|
||||
- resource type and parent links
|
||||
- path, labels, trust zone, owner, and durable backend metadata
|
||||
- `metadata.flex_auth_contract: resource-registration-v0`
|
||||
- `caring_profile: caring-0.4.0-rc2` when the emitter can provide it
|
||||
|
||||
flex-auth imports the manifest through `internal/markitect.ImportResourceManifest`.
|
||||
The importer enriches resources with CARING scope and plane classification and
|
||||
returns diagnostics when a resource type, trust zone, label set, or CARING
|
||||
profile is missing or ambiguous.
|
||||
|
||||
## 2. Submit Check Requests
|
||||
|
||||
For one resource, Markitect submits `CheckRequest`:
|
||||
|
||||
```text
|
||||
subject + action + resource + context + optional caring_context
|
||||
```
|
||||
|
||||
For repeated checks, Markitect submits `BatchCheckRequest` with the same
|
||||
subject/action/context and a resource list. Resource order is preserved in the
|
||||
response.
|
||||
|
||||
Markitect should pass local frontmatter and backend metadata as resource
|
||||
attributes when they affect policy:
|
||||
|
||||
- `labels`
|
||||
- `trust_zone`
|
||||
- `markitect_path`
|
||||
- `frontmatter_visibility`
|
||||
- `source_revision`
|
||||
- `workflow_state`
|
||||
- `freshness_seconds`
|
||||
- `data_classes`
|
||||
|
||||
Subject roles and groups should be normalized into subject attributes or loaded
|
||||
into the registry as subject/group/team manifests. CARING dimensions should be
|
||||
attached as `caring_context` when Markitect already knows the intended
|
||||
descriptor; otherwise flex-auth can derive descriptors from relationship facts.
|
||||
|
||||
## 3. Enforce Decisions
|
||||
|
||||
Markitect maps flex-auth decision envelopes into its gateway contract with
|
||||
`internal/markitect.ToGatewayDecision`.
|
||||
|
||||
Expected effects:
|
||||
|
||||
- `allow`: render, answer, package, activate, export, or run workflow.
|
||||
- `deny`: block the operation.
|
||||
- `redact`: continue with returned obligations such as `mask_fields`.
|
||||
- `audit_denied`: block or quarantine while preserving an audit-grade record.
|
||||
|
||||
Markitect should enforce obligations before exposing data. For example, a
|
||||
`mask_fields` obligation must be applied before rendering or model use, and a
|
||||
`record_context_activation` obligation must be logged when a context package is
|
||||
activated.
|
||||
|
||||
## 4. Record Decision IDs
|
||||
|
||||
Every gateway operation should persist the flex-auth decision id alongside the
|
||||
Markitect request id, workflow id, rendered artifact id, or export id.
|
||||
|
||||
At minimum, always record:
|
||||
|
||||
- denies
|
||||
- redactions
|
||||
- exports
|
||||
- context package activations
|
||||
- workflow runs
|
||||
- support, break-glass, or other CARING exposure events
|
||||
|
||||
The local flex-auth JSONL log is suitable for local development. Markitect may
|
||||
also write the same decision id into its own event log.
|
||||
|
||||
## 5. Explain Decisions
|
||||
|
||||
When users need an explanation, Markitect can call flex-auth `explain` or use
|
||||
the decision id to retrieve the persisted envelope and project it with
|
||||
`ToGatewayDecision`.
|
||||
|
||||
Explanations should preserve CARING language:
|
||||
|
||||
- subject relation and canonical role, for example `Customer Doer`
|
||||
- scope, for example `Resource document:internal-note`
|
||||
- plane, for example `Data`
|
||||
- capability, for example `View` or `Export`
|
||||
- exposure mode, for example `Masked`, `Plaintext`, or `Exportable`
|
||||
- restrictions, conditions, and conformance findings
|
||||
|
||||
Example explanation:
|
||||
|
||||
```text
|
||||
Customer Doer may View Data Plane resource document:internal-note because reader_group.
|
||||
```
|
||||
|
||||
## Local Mapping Summary
|
||||
|
||||
| Markitect local concept | flex-auth field | CARING dimension |
|
||||
| --- | --- | --- |
|
||||
| frontmatter visibility | `resource.attributes.frontmatter_visibility` | Exposure mode hint |
|
||||
| document labels | `resource.attributes.labels` | Scope and exposure hint |
|
||||
| owner/steward | `resource.owner`, subject groups/roles | Canonical role and relation |
|
||||
| workflow state | `resource.attributes.workflow_state`, request context | Lifecycle/condition hint |
|
||||
| context freshness | `request.context.freshness_seconds` | Condition and conformance finding |
|
||||
| export request | `action: export` | `Export` capability and `Exportable` exposure |
|
||||
|
||||
The examples in `examples/markitect/` are the executable contract fixtures for
|
||||
this flow.
|
||||
76
docs/markitect-resource-namespace.md
Normal file
76
docs/markitect-resource-namespace.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Markitect Resource Namespace
|
||||
|
||||
This document defines the Markitect protected-system namespace consumed by
|
||||
flex-auth. It is the P3.1 contract between Markitect resource metadata and the
|
||||
generic flex-auth registry.
|
||||
|
||||
The namespace is intentionally Markitect-specific at the edge and generic once
|
||||
registered. Markitect may keep its local frontmatter and backend metadata
|
||||
names, but emitted resource manifests should normalize them into the resource
|
||||
types and CARING dimensions below.
|
||||
|
||||
## Hierarchy
|
||||
|
||||
```text
|
||||
knowledge_base
|
||||
-> repository
|
||||
-> document
|
||||
-> section
|
||||
-> span
|
||||
-> context_package
|
||||
-> workflow_artifact
|
||||
-> export
|
||||
```
|
||||
|
||||
Markitect may emit a partial tree. For example, a document can be parented
|
||||
directly to a knowledge base when the repository boundary is not material to a
|
||||
policy decision. flex-auth treats `parent` as a stable relationship hint; P3.2
|
||||
and P3.4 add importer and check fixtures that make inherited behavior explicit.
|
||||
|
||||
## CARING Mapping
|
||||
|
||||
| Markitect resource type | Parent types | CARING scope | CARING planes | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `knowledge_base` | none | `Workspace` | `Intent`, `Data` | Top-level user-visible knowledge container. |
|
||||
| `repository` | `knowledge_base` | `Project` | `Build`, `Data` | Versioned source or storage boundary behind a knowledge base. |
|
||||
| `document` | `repository`, `knowledge_base` | `Resource` | `Data` | Renderable document or page. Markitect `path` maps to resource `path`. |
|
||||
| `section` | `document` | `Subresource` | `Data` | Stable heading or block region inside a document. |
|
||||
| `span` | `section`, `document` | `Field` | `Data` | Fine-grained text range, cell, token span, or field-level surface. |
|
||||
| `context_package` | `knowledge_base`, `repository`, `document` | `Dataset` | `Intent`, `Data`, `Policy` | Bundled context prepared for model/tool use. |
|
||||
| `workflow_artifact` | `context_package`, `document` | `Process` | `Execution`, `Data`, `Audit` | Generated workflow output, review artifact, or intermediate. |
|
||||
| `export` | `workflow_artifact`, `context_package`, `document` | `Record` | `Data`, `Audit` | Materialized package, file, archive, or external transfer. |
|
||||
|
||||
## Frontmatter Compatibility
|
||||
|
||||
Markitect document frontmatter can remain local, but manifests should preserve
|
||||
the following mappings:
|
||||
|
||||
- `id` or stable slug -> `resources[].id`
|
||||
- document kind -> `resources[].type`
|
||||
- source path -> `resources[].path`
|
||||
- parent knowledge base, repository, or document -> `resources[].parent`
|
||||
- labels, classification, or visibility -> `resources[].labels`
|
||||
- tenant/customer boundary -> `resources[].attributes.tenant` when it is not
|
||||
already represented by the request subject/resource tenant
|
||||
- owner team or steward -> `resources[].owner`
|
||||
- freshness, workflow state, and source revision -> `resources[].attributes`
|
||||
|
||||
## Backend Metadata Compatibility
|
||||
|
||||
Backend metadata can be richer than the flex-auth contract. The manifest should
|
||||
keep durable values in `attributes` and avoid embedding backend-only transient
|
||||
state in resource ids.
|
||||
|
||||
Recommended backend metadata keys:
|
||||
|
||||
- `markitect_path`
|
||||
- `frontmatter_visibility`
|
||||
- `source_revision`
|
||||
- `workflow_state`
|
||||
- `freshness_seconds`
|
||||
- `data_classes`
|
||||
- `tenant`
|
||||
|
||||
The examples in `examples/markitect/protected_system_manifest.yaml` and
|
||||
`examples/markitect/namespace_resource_manifest.yaml` are the pinned schema
|
||||
examples for this namespace.
|
||||
104
docs/ops-warden-policy-gate-handoff.md
Normal file
104
docs/ops-warden-policy-gate-handoff.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Ops-Warden Policy Gate Handoff
|
||||
|
||||
Date: 2026-06-23
|
||||
Workplan: FLEX-WP-0006
|
||||
Ops-warden unblocker: WARDEN-WP-0009 T01
|
||||
|
||||
## Published flex-auth assets
|
||||
|
||||
- Policy package: examples/ops-warden/policy_package.md
|
||||
- Policy fixtures: examples/ops-warden/policy_fixtures.yaml
|
||||
- Combined registry fixture: examples/ops-warden/registry_snapshot.json
|
||||
- Protected-system manifest: examples/ops-warden/protected_system_manifest.yaml
|
||||
- Resource manifest: examples/ops-warden/resource_manifest.yaml
|
||||
- Subject manifest: examples/ops-warden/subject_manifest.yaml
|
||||
- Service request fixtures: examples/ops-warden/check_request_*.json
|
||||
|
||||
## Local service command
|
||||
|
||||
flex-auth serve --addr 127.0.0.1:8080 --registry examples/ops-warden/registry_snapshot.json --policy examples/ops-warden/policy_package.md --log /tmp/flex-auth-ops-warden-decisions.jsonl
|
||||
|
||||
Ops-warden can point policy.flex_auth_url at that base URL for local smoke.
|
||||
Production should keep policy.fail_closed true unless an explicit break-glass
|
||||
procedure exists.
|
||||
|
||||
## Fixture coverage
|
||||
|
||||
Allow fixtures:
|
||||
|
||||
- fixture:ops-warden-adm-sign-allow
|
||||
- fixture:ops-warden-agt-sign-allow
|
||||
- fixture:ops-warden-atm-sign-allow
|
||||
|
||||
Deny fixtures:
|
||||
|
||||
- fixture:ops-warden-unknown-subject-deny
|
||||
- fixture:ops-warden-actor-type-mismatch-deny
|
||||
- fixture:ops-warden-ttl-above-max-deny
|
||||
- fixture:ops-warden-disallowed-principal-deny
|
||||
- fixture:ops-warden-missing-fingerprint-deny
|
||||
|
||||
## Non-secret smoke evidence
|
||||
|
||||
CLI validation on 2026-06-23:
|
||||
|
||||
- protected-system manifest: valid
|
||||
- resource manifest: valid
|
||||
- subject manifest: valid
|
||||
- registry snapshot: loaded 1 system, 1 resource manifest, 3 subjects,
|
||||
3 groups, 3 relationships, and 1 tenant
|
||||
- policy package: valid with 8 passing fixtures
|
||||
|
||||
Local /v1/check service smoke on 2026-06-23:
|
||||
|
||||
- allow request: effect allow, reason signing_policy_matched,
|
||||
decision id decision:706efe49f68d9ef1
|
||||
- deny request: effect deny, reason ttl_out_of_bounds,
|
||||
decision id decision:b69bdc25a988f367
|
||||
- GET /v1/check: HTTP 405
|
||||
- malformed POST /v1/check: HTTP 400
|
||||
- decision log contained both decision ids
|
||||
|
||||
## Production sequence for ops-warden
|
||||
|
||||
1. Deploy the flex-auth registry and policy package above to the selected
|
||||
flex-auth runtime.
|
||||
2. Configure ops-warden policy.flex_auth_url to the flex-auth base URL.
|
||||
3. Set policy.enabled: true.
|
||||
4. Keep policy.tenant as tenant:platform unless a tenant-specific policy package
|
||||
is introduced.
|
||||
5. Run one allow-path sign smoke and confirm signatures.log includes
|
||||
policy_decision_id.
|
||||
6. Run one deny-path smoke with fail_closed true and preserve only non-secret
|
||||
evidence.
|
||||
|
||||
## Ownership boundary
|
||||
|
||||
flex-auth owns the authorization decision for the signing request. ops-warden
|
||||
continues to own actor inventory, SSH CA operation, OpenBao SSH engine
|
||||
integration, host documentation, and signatures.log production evidence.
|
||||
|
||||
No SSH private keys, OpenBao tokens, database credentials, or real public-key
|
||||
material are stored in these fixtures.
|
||||
|
||||
|
||||
## FLEX-WP-0007 Production Update
|
||||
|
||||
Additional published assets:
|
||||
|
||||
- Production registry fixture: examples/ops-warden/production_registry_snapshot.json
|
||||
- Registry sync runbook: docs/ops-warden-registry-sync.md
|
||||
|
||||
Production runtime command:
|
||||
|
||||
flex-auth serve --addr 0.0.0.0:8080 --registry examples/ops-warden/production_registry_snapshot.json --policy examples/ops-warden/policy_package.md --log /var/log/flex-auth/ops-warden-decisions.jsonl
|
||||
|
||||
Use http://flex-auth.flex-auth.svc.cluster.local:8080 when cluster DNS is
|
||||
reachable from warden workstations. Otherwise use the approved operator tunnel
|
||||
or ingress URL. Always pre-flight GET /healthz from the same workstation before
|
||||
enabling policy.enabled with fail_closed true.
|
||||
|
||||
Production actor coverage now verifies agt-state-hub-bridge,
|
||||
agt-codex-interhub-bootstrap, adm-example, atm-backup-daily, ttl_out_of_bounds,
|
||||
unknown_actor_resource, and the iam:agt-state-hub-bridge subject path used by
|
||||
WARDEN_POLICY_SUBJECT.
|
||||
128
docs/ops-warden-registry-sync.md
Normal file
128
docs/ops-warden-registry-sync.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Ops-Warden Registry Sync
|
||||
|
||||
Date: 2026-06-23
|
||||
Workplan: FLEX-WP-0007
|
||||
|
||||
This is the flex-auth side of the production policy gate runbook for ops-warden
|
||||
SSH signing. ops-warden owns actor inventory and generated registry content;
|
||||
flex-auth hosts that registry, evaluates the policy package, and returns the
|
||||
decision envelope used by warden sign.
|
||||
|
||||
## Production Runtime Target
|
||||
|
||||
Use the NetKingdom operator-reachable service URL as the canonical
|
||||
policy.flex_auth_url. The preferred target is an in-cluster flex-auth Service
|
||||
fronted by the existing operator access path:
|
||||
|
||||
http://flex-auth.flex-auth.svc.cluster.local:8080
|
||||
|
||||
If cluster DNS is not reachable from the workstation that runs warden sign, use
|
||||
an approved operator tunnel or ingress URL with the same base path semantics. Do
|
||||
not turn on policy.enabled with fail_closed true until this pre-flight succeeds
|
||||
from the same workstation:
|
||||
|
||||
curl -fsS <policy.flex_auth_url>/healthz
|
||||
|
||||
Start the runtime with the production registry snapshot and the ops-warden
|
||||
policy package:
|
||||
|
||||
flex-auth serve --addr 0.0.0.0:8080 --registry examples/ops-warden/production_registry_snapshot.json --policy examples/ops-warden/policy_package.md --log /var/log/flex-auth/ops-warden-decisions.jsonl
|
||||
|
||||
The checked-in production snapshot is a non-secret fixture and initial load
|
||||
target. Regenerate it from ops-warden inventory whenever actors, principals, or
|
||||
TTL defaults change.
|
||||
|
||||
## Current Operator Tunnel
|
||||
|
||||
As of 2026-06-24, the reachable operator-tunnel URL for CoulombCore is:
|
||||
|
||||
http://127.0.0.1:18090
|
||||
|
||||
The tunnel name is flex-auth-coulombcore. It forwards CoulombCore
|
||||
127.0.0.1:18090 to the local flex-auth runtime on 127.0.0.1:18090. Verified
|
||||
checks from CoulombCore:
|
||||
|
||||
- GET /healthz returned HTTP 200.
|
||||
- POST /v1/check for agt-state-hub-bridge returned allow with decision:873c6c682a52bebc.
|
||||
|
||||
This is an operator tunnel pattern, not a substitute for a future in-cluster
|
||||
Service if flex-auth should run inside the cluster.
|
||||
|
||||
## Ownership Contract
|
||||
|
||||
| Concern | Owner | Notes |
|
||||
| --- | --- | --- |
|
||||
| Actor names and actor types | ops-warden | inventory.yaml defines adm, agt, and atm actors. |
|
||||
| Default principals and TTLs | ops-warden | Used by warden sign and by generated registry attributes. |
|
||||
| Registry hosting and reload | flex-auth | Runtime serves the generated snapshot and evaluates it with the policy package. |
|
||||
| Policy package semantics | flex-auth | examples/ops-warden/policy_package.md owns allow and deny reasons. |
|
||||
| OpenBao SSH signing | ops-warden | flex-auth never receives SSH private keys or Vault tokens. |
|
||||
| Production policy.enabled flip | ops-warden operator | Only after healthz and allow/deny smoke pass. |
|
||||
|
||||
## Sync Procedure
|
||||
|
||||
1. In ops-warden, update the managed inventory source or ~/.config/warden/inventory.yaml.
|
||||
2. Regenerate the flex-auth snapshot from ops-warden:
|
||||
|
||||
python scripts/build_flex_auth_registry.py ~/.config/warden/inventory.yaml -o registry/flex-auth/production_registry_snapshot.json
|
||||
|
||||
3. Validate the generated file before handoff:
|
||||
|
||||
flex-auth load-registry --file registry/flex-auth/production_registry_snapshot.json
|
||||
|
||||
4. Copy or promote the snapshot to the flex-auth runtime. For repo-level drift
|
||||
coverage, update examples/ops-warden/production_registry_snapshot.json when
|
||||
the intended production fixture changes.
|
||||
5. Restart or reload the flex-auth runtime with the new snapshot.
|
||||
6. From the workstation that runs warden sign, verify:
|
||||
|
||||
curl -fsS <policy.flex_auth_url>/healthz
|
||||
|
||||
7. Run one allow smoke and one deny smoke. Record only non-secret evidence:
|
||||
actor name, decision id, effect, reason, backend, and whether a certificate
|
||||
was issued.
|
||||
|
||||
## Current Production Fixture
|
||||
|
||||
The initial fixture mirrors ops-warden production inventory as of 2026-06-23.
|
||||
It registers:
|
||||
|
||||
| Actor | Type | Principal | Max TTL hours | Allowed subjects |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| adm-example | adm | adm-full | 48 | adm-example, iam:adm-example |
|
||||
| agt-codex-interhub-bootstrap | agt | agt-interhub-bootstrap | 2 | agt-codex-interhub-bootstrap, iam:agt-codex-interhub-bootstrap |
|
||||
| agt-state-hub-bridge | agt | agt-task-bridge | 24 | agt-state-hub-bridge, iam:agt-state-hub-bridge |
|
||||
| atm-backup-daily | atm | atm-backup-daily | 8 | atm-backup-daily, iam:atm-backup-daily |
|
||||
|
||||
The IAM subject form is intended for WARDEN_POLICY_SUBJECT. If that environment
|
||||
variable is unset, ops-warden sends the actor name and the same policy path
|
||||
continues to work.
|
||||
|
||||
## Smoke Expectations
|
||||
|
||||
Allow path:
|
||||
|
||||
warden sign agt-state-hub-bridge
|
||||
|
||||
Expected non-secret evidence: decision effect allow, reason
|
||||
signing_policy_matched, signatures.log includes policy_decision_id.
|
||||
|
||||
Deny path:
|
||||
|
||||
warden sign agt-state-hub-bridge --ttl 999
|
||||
|
||||
Expected non-secret evidence: effect deny, reason ttl_out_of_bounds, no
|
||||
certificate issued. With fail_closed true, unreachable flex-auth must also block
|
||||
signing.
|
||||
|
||||
OpenBao-backed signing remains an operator smoke because it requires a scoped
|
||||
VAULT_TOKEN. The previous session returned HTTP 403 on 2026-06-23; retry with:
|
||||
|
||||
SMOKE_VAULT=1 ~/ops-warden/scripts/policy_gate_production_smoke.sh
|
||||
|
||||
## References
|
||||
|
||||
- docs/ops-warden-policy-gate-handoff.md
|
||||
- examples/ops-warden/production_registry_snapshot.json
|
||||
- ~/ops-warden/wiki/PolicyGatedSigning.md
|
||||
- ~/ops-warden/history/2026-06-23-flex-auth-policy-gate-production-smoke.md
|
||||
142
docs/pre-implementation-assessment.md
Normal file
142
docs/pre-implementation-assessment.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# flex-auth Pre-Implementation Assessment
|
||||
|
||||
Date: 2026-05-15
|
||||
Author: Claude (Opus 4.7) with Bernd
|
||||
Status: Accepted — feeds FLEX-WP-0005 Foundations and the refresh of FLEX-WP-0002/0004
|
||||
|
||||
## Purpose
|
||||
|
||||
Captures the SWOT and boundary review performed before flex-auth code work
|
||||
begins. The conclusions of this assessment are turned into three ADRs
|
||||
(`docs/adr/0001`-`0003`) and a new foundations workplan
|
||||
(`workplans/FLEX-WP-0005`), which together precede the standalone core
|
||||
(`FLEX-WP-0002`).
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
The repository is planning-mature and code-blank. INTENT, SCOPE, the PRD,
|
||||
the authorization-landscape research note, and the four initial workplans
|
||||
are internally consistent and mirrored in the State Hub. `FLEX-WP-0001` is
|
||||
done.
|
||||
|
||||
Starting implementation directly at `FLEX-WP-0002 P2.1` is reasonable in
|
||||
shape but premature in detail: several decisions the workplans leave open
|
||||
would be made implicitly by the first commit. Those decisions are pulled
|
||||
forward into `FLEX-WP-0005` so the standalone core lands on settled
|
||||
foundations.
|
||||
|
||||
## SWOT
|
||||
|
||||
### Strengths
|
||||
|
||||
- Ownership boundary stated and repeated consistently:
|
||||
- **key-cape / NetKingdom SSO** owns identity.
|
||||
- **flex-auth** owns authorization.
|
||||
- **protected systems** own enforcement.
|
||||
- Backend-neutral vocabulary commitment: Topaz, OpenFGA, SpiceDB, OPA,
|
||||
Cedar, and Keycloak Authorization Services are framed as adapters, not
|
||||
the product.
|
||||
- Concrete first consumer (Markitect) with its side already in flight
|
||||
(`MKTT-WP-0014`).
|
||||
- Standalone-first mode keeps flex-auth useful before any enterprise PDP
|
||||
is wired in.
|
||||
- Hard problems named, not papered over: group overage, directory
|
||||
freshness, fail-open vs fail-closed, stale/partial/uncertain decisions,
|
||||
explain APIs, audit-only versus deny.
|
||||
- State Hub integration in place from day one (workstream and task IDs
|
||||
in workplan frontmatter, custodian brief committed, dispatch active).
|
||||
|
||||
### Weaknesses
|
||||
|
||||
- No implementation language or repo skeleton committed.
|
||||
- Policy package format is described as "a simple declarative rule format
|
||||
with room for OPA/Rego, Cedar, and Topaz later" — central artefact, but
|
||||
unpinned.
|
||||
- No ADRs. `net-kingdom/DECISIONS.md` and the key-cape spec habit are not
|
||||
mirrored here yet.
|
||||
- The `FlexAuthResourceManifest` is referenced as already implemented on
|
||||
the Markitect side without being pinned in this repo. Cross-repo
|
||||
contract drift risk.
|
||||
- NetKingdom IAM Profile
|
||||
(`~/the-custodian/canon/standards/iam-profile_v0.1.md`) is only cited
|
||||
at the bottom of the research note — it is the upstream identity
|
||||
contract flex-auth consumes and deserves first-class citation.
|
||||
- No project skeleton, Makefile, lint, CI, or SBOM yet.
|
||||
- Emergency principal / break-glass listed as a first-class subject type
|
||||
with no mechanics described.
|
||||
|
||||
### Opportunities
|
||||
|
||||
- Markitect is aligned and waiting. Tight feedback loop available.
|
||||
- `explain(decision_id)` is a real differentiator versus Topaz, Cerbos,
|
||||
and OPA in isolation. Literate, reviewable policy packages amplify the
|
||||
same lever.
|
||||
- CLI-first standalone mode can ship usefully across NetKingdom repos
|
||||
early, before service mode lands.
|
||||
- Register flex-auth as a State Hub capability with extension points so
|
||||
Markitect and later consumers discover it natively.
|
||||
|
||||
### Threats / Risks
|
||||
|
||||
- **Thin-wrapper-around-Topaz trap.** Topaz already combines OPA/Rego,
|
||||
local directory, and relations. If the standalone core reimplements
|
||||
60% of Topaz badly and then adapts to Topaz anyway, the abstraction
|
||||
earns nothing. The escape is to make the *registry + audit + explain +
|
||||
multi-consumer* surface the actual product, and to align the
|
||||
standalone evaluator with Rego from day one so the later Topaz
|
||||
adapter is a small step.
|
||||
- Markitect-side manifest exists; flex-auth has not pinned it. Easy to
|
||||
lock in the wrong shape.
|
||||
- Schedule coupling: `FLEX-WP-0003` is blocked on `0002`. Every week of
|
||||
core slippage is a week Markitect waits.
|
||||
- "Yet another authz layer" perception if a bespoke rules format ships
|
||||
before the Topaz/Rego direction is recorded.
|
||||
|
||||
## Boundary Review
|
||||
|
||||
| Repo | Owns | Overlap with flex-auth | Verdict |
|
||||
| --- | --- | --- | --- |
|
||||
| `key-cape` | OIDC/PKCE, MFA, token lifecycle, NetKingdom IAM Profile impl, coarse roles and scopes | flex-auth consumes verified claims as inputs; coarse roles live in key-cape, resource-specific decisions in flex-auth | Clean. IAM Profile citation made explicit. |
|
||||
| `net-kingdom` | Security core, Keycloak (heavy mode), IAM Profile spec, canon | Keycloak Authorization Services is itself a PDP. flex-auth's research recommends "Keycloak as SSO only, flex-auth owns authorization" as the canonical pattern, with Keycloak AuthZ available as one adapter. | Pinned in ADR-003 / FLEX-WP-0004. Not a boundary problem, a recorded decision. |
|
||||
| `ops-bridge` | SSH reverse tunnels, connectivity | Disclaims being a credential authority or policy engine. | No overlap. |
|
||||
| `ops-warden` | SSH cert CA for `adm`/`agt`/`atm` actors; short-lived SSH certificates | Different identity universe (SSH actors, not OIDC subjects). An `agt` authenticated via warden SSH cert may later appear as a flex-auth subject in some flow, but the two surfaces do not collide. | No overlap. Boundary line added to SCOPE.md. |
|
||||
|
||||
## Refinements Adopted
|
||||
|
||||
1. **ADR-001** — Implementation language & repo skeleton: Go, aligned with
|
||||
key-cape's vindicated language decision.
|
||||
2. **ADR-002** — Policy-package format: Rego-in-Markdown, from day one.
|
||||
Literate policy packages co-locate intent, rules, and tests.
|
||||
3. **ADR-003** — MVP backend alignment: shape the standalone core to be
|
||||
Rego/Topaz-aligned so the later Topaz adapter is a small step.
|
||||
4. **FLEX-WP-0005 Foundations** is inserted between `0001` (done) and
|
||||
`0002` (core). It performs the Topaz spike *before* the core's policy
|
||||
loader and check API are written, pins the resource manifest schema,
|
||||
and lands the repo skeleton.
|
||||
5. **INTENT/SCOPE** cite the NetKingdom IAM Profile explicitly and record
|
||||
the ops-warden boundary.
|
||||
|
||||
## Sequencing After Refinement
|
||||
|
||||
```text
|
||||
FLEX-WP-0001 done Repo intent and authorization-landscape baseline
|
||||
FLEX-WP-0005 todo P0 Foundations and Topaz alignment (ADRs, skeleton,
|
||||
spike, manifest pinning)
|
||||
FLEX-WP-0002 blocked Standalone policy-as-code core, Rego-in-Markdown
|
||||
FLEX-WP-0003 blocked Markitect consumer integration
|
||||
FLEX-WP-0004 blocked Delegated PDP and directory adapters (Topaz
|
||||
evaluation now in 0005)
|
||||
```
|
||||
|
||||
## Traceability
|
||||
|
||||
- `INTENT.md`, `SCOPE.md`, `README.md`, `.custodian-brief.md`
|
||||
- `docs/ProductRequirementsDocument.md`
|
||||
- `docs/flex-auth-authorization-registry-research.md`
|
||||
- `docs/workplan-planning-map.md`
|
||||
- `docs/adr/0001-implementation-language-and-skeleton.md` *(new)*
|
||||
- `docs/adr/0002-rego-in-markdown-policy-format.md` *(new)*
|
||||
- `docs/adr/0003-topaz-aligned-mvp.md` *(new)*
|
||||
- `workplans/FLEX-WP-0001-…`, `0002-…`, `0003-…`, `0004-…`
|
||||
- `workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md` *(new)*
|
||||
- NetKingdom IAM Profile: `~/the-custodian/canon/standards/iam-profile_v0.1.md`
|
||||
105
docs/relationship-pdp-adapter-boundary.md
Normal file
105
docs/relationship-pdp-adapter-boundary.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Relationship PDP Adapter Boundary
|
||||
|
||||
Status: implemented for FLEX-WP-0004 P4.2.
|
||||
|
||||
## Role
|
||||
|
||||
The relationship PDP adapter is the common boundary for OpenFGA,
|
||||
SpiceDB, and other tuple-oriented authorization systems. These backends
|
||||
answer questions like:
|
||||
|
||||
```text
|
||||
is subject S related to object O through relation R?
|
||||
```
|
||||
|
||||
flex-auth keeps the protected-system API stable. The adapter translates
|
||||
`CheckRequest`, `BatchCheckRequest`, registry relationships, and
|
||||
`list_allowed` queries into tuple checks, then wraps backend responses
|
||||
back into the canonical `DecisionEnvelope`.
|
||||
|
||||
## Tuple Mapping
|
||||
|
||||
Canonical flex-auth relationship facts map to:
|
||||
|
||||
| flex-auth | tuple field |
|
||||
| --- | --- |
|
||||
| resource type | `object_type` |
|
||||
| resource id | `object_id` |
|
||||
| action or relation | `relation` |
|
||||
| subject type | `subject_type` |
|
||||
| subject id | `subject_id` |
|
||||
| group membership indirection | `subject_relation=member` |
|
||||
|
||||
Registry group and team membership become `group#member` tuples.
|
||||
Resource parent edges become `parent` tuples. Resource owners become
|
||||
`owner_team` tuples. Relationship facts keep conditions, provenance,
|
||||
metadata, and CARING descriptors on the imported tuple so backend
|
||||
results can preserve explanatory context.
|
||||
|
||||
## Inheritance
|
||||
|
||||
Relationship backends represent inheritance differently:
|
||||
|
||||
- OpenFGA usually models it in the authorization model through rewrites.
|
||||
- SpiceDB usually models it through relation definitions and caveats.
|
||||
- flex-auth records inherited evidence in
|
||||
`TupleCheckResult.InheritedFrom`.
|
||||
|
||||
The envelope does not expose backend-native rewrite syntax. It records
|
||||
the fact that inheritance participated through diagnostics and preserves
|
||||
the matched CARING descriptor from direct or inherited tuples.
|
||||
|
||||
## Batch And List
|
||||
|
||||
`BatchCheck` preserves request order. If the backend returns a partial
|
||||
batch, flex-auth emits fail-closed deny envelopes for the affected
|
||||
resources.
|
||||
|
||||
`ListAllowed` returns a `ListAllowedResult` containing allow envelopes,
|
||||
the backend consistency token, and diagnostics. It intentionally returns
|
||||
envelopes instead of raw resource ids so downstream consumers keep the
|
||||
same audit and CARING metadata they receive from single checks.
|
||||
|
||||
## Consistency Metadata
|
||||
|
||||
Tuple backends expose different consistency tokens:
|
||||
|
||||
- OpenFGA: model id plus optional tuple-store continuation/freshness
|
||||
metadata.
|
||||
- SpiceDB: zedtoken.
|
||||
|
||||
The adapter stores the backend token in
|
||||
`DecisionEnvelope.provenance.directory_etag`. The field name is kept for
|
||||
compatibility with the existing flex-auth envelope; for relationship PDPs
|
||||
it means "relationship backend consistency token".
|
||||
|
||||
## Failure Behavior
|
||||
|
||||
The adapter fails closed for:
|
||||
|
||||
- backend unavailable: `relationship_backend_unavailable`
|
||||
- stale consistency token: `relationship_data_stale`
|
||||
- partial backend result: `relationship_partial_result`
|
||||
- untranslatable request: `relationship_request_incomplete`
|
||||
|
||||
Each failure is a deny envelope with `diagnostics.relationship_failure`
|
||||
and a CARING conformance finding. This keeps delegated tuple behavior
|
||||
aligned with standalone fail-closed behavior.
|
||||
|
||||
## CARING Preservation
|
||||
|
||||
Tuple systems do not understand CARING directly. flex-auth therefore
|
||||
keeps CARING metadata at the adapter boundary:
|
||||
|
||||
- request descriptor wins when supplied;
|
||||
- backend result descriptor is next;
|
||||
- matched tuple descriptor is next;
|
||||
- inherited tuple descriptor is next;
|
||||
- otherwise the envelope includes
|
||||
`RELATIONSHIP-CARING-DESCRIPTOR-MISSING`.
|
||||
|
||||
The adapter copies scope, planes, capabilities, exposure modes,
|
||||
restrictions, derived capabilities, conformance findings, and
|
||||
exposure-event hooks into the decision envelope. Backend-native role or
|
||||
relation names should not leak into protected systems as a replacement
|
||||
for CARING canonical roles.
|
||||
103
docs/rule-pdp-adapter-boundary.md
Normal file
103
docs/rule-pdp-adapter-boundary.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Rule PDP Adapter Boundary
|
||||
|
||||
Status: implemented for FLEX-WP-0004 P4.3.
|
||||
|
||||
## Role
|
||||
|
||||
The rule PDP adapter is the common boundary for OPA/Rego, Cedar-style
|
||||
policy services, and other engines that evaluate a policy language over
|
||||
a structured request. It is separate from the relationship-PDP boundary:
|
||||
relationship backends answer tuple reachability questions, while rule
|
||||
backends evaluate policy logic over subject, action, resource, context,
|
||||
and CARING metadata.
|
||||
|
||||
## Canonical Input
|
||||
|
||||
All rule backends receive the same canonical input shape:
|
||||
|
||||
```text
|
||||
input.subject
|
||||
input.action
|
||||
input.resource
|
||||
input.context
|
||||
input.caring_context
|
||||
input.policy.package
|
||||
input.policy.version
|
||||
```
|
||||
|
||||
OPA/Rego can consume this shape directly. Cedar adapters translate the
|
||||
same fields into principal/action/resource/context entities at the
|
||||
backend boundary. Protected systems do not see backend-native input
|
||||
syntax.
|
||||
|
||||
## Policy Artifacts
|
||||
|
||||
`PolicyArtifactFromPackage` converts a validated Rego-in-Markdown
|
||||
package into a delegated artifact:
|
||||
|
||||
- `language=rego`
|
||||
- package id and version from frontmatter
|
||||
- extracted Rego module unchanged
|
||||
- test blocks and fixtures preserved
|
||||
- CARING policy metadata preserved
|
||||
|
||||
Cedar and other rule engines use the same `PolicyArtifact` envelope,
|
||||
but may reject unsupported artifacts with `rule_policy_unsupported`.
|
||||
|
||||
## Fixtures
|
||||
|
||||
`EvaluateFixtures` runs `api.PolicyFixture` values through the delegated
|
||||
adapter and compares the returned effect, reason, and obligations. This
|
||||
keeps delegated backends honest against the same fixtures used by the
|
||||
standalone evaluator.
|
||||
|
||||
## Obligations And Diagnostics
|
||||
|
||||
Rule backends can return obligations such as masking, audit, or approval
|
||||
requirements. The adapter copies them into the canonical
|
||||
`DecisionEnvelope`. Backend diagnostics are preserved and supplemented
|
||||
with:
|
||||
|
||||
- `adapter=rule`
|
||||
- backend name
|
||||
- delegated mode
|
||||
- language
|
||||
- policy package and version
|
||||
- fail-closed reason when present
|
||||
|
||||
## Versioning
|
||||
|
||||
The envelope records backend policy version in
|
||||
`matched_policy_version` and `provenance.policy_version`. A backend may
|
||||
return a newer concrete revision than the request asked for; the adapter
|
||||
records what actually matched.
|
||||
|
||||
## Failure Behavior
|
||||
|
||||
The adapter fails closed for:
|
||||
|
||||
- backend unavailable: `rule_backend_unavailable`
|
||||
- stale policy: `rule_policy_stale`
|
||||
- partial result: `rule_partial_result`
|
||||
- invalid input: `rule_request_incomplete`
|
||||
- unsupported policy artifact: `rule_policy_unsupported`
|
||||
|
||||
Each failure returns a deny envelope with `diagnostics.rule_failure` and
|
||||
a CARING conformance finding.
|
||||
|
||||
## CARING Preservation
|
||||
|
||||
Rule engines vary in how much of CARING they can represent natively.
|
||||
flex-auth keeps CARING outside the backend-specific language contract:
|
||||
|
||||
- request descriptor wins;
|
||||
- backend result descriptor is next;
|
||||
- policy frontmatter supplies profile and expected dimensions;
|
||||
- gaps become `RULE-CARING-METADATA-GAP` or
|
||||
`RULE-CARING-DESCRIPTOR-MISSING` findings.
|
||||
|
||||
The decision envelope preserves descriptor, scope, planes,
|
||||
capabilities, exposure modes, restrictions, derived capabilities,
|
||||
conformance findings, exposure-event hooks, obligations, and diagnostics.
|
||||
Backend-native policy names should never replace canonical CARING roles
|
||||
in protected-system responses.
|
||||
107
docs/topaz-adapter-operations.md
Normal file
107
docs/topaz-adapter-operations.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Topaz Adapter Operations
|
||||
|
||||
Status: implemented for FLEX-WP-0004 P4.1.
|
||||
|
||||
## Role
|
||||
|
||||
The Topaz adapter is a delegated PDP and directory adapter behind the
|
||||
stable flex-auth API. Protected systems still send `CheckRequest`,
|
||||
`BatchCheckRequest`, registry snapshots, and Rego-in-Markdown policy
|
||||
packages to flex-auth. The adapter translates those into Topaz directory
|
||||
objects, relations, permission checks, and an OPA bundle, then wraps the
|
||||
result back into the same `DecisionEnvelope` used by standalone mode.
|
||||
|
||||
## Wire Protocol
|
||||
|
||||
The production recommendation remains gRPC because it is Topaz's native
|
||||
API and gives the strongest typed client surface for authorizer,
|
||||
reader, writer, and model operations. The implementation added in P4.1
|
||||
keeps that choice behind `internal/adapters/topaz.Client` and ships an
|
||||
HTTP REST client for the runnable `examples/topaz` topology.
|
||||
|
||||
This split is deliberate:
|
||||
|
||||
- `Client` is the stable flex-auth boundary.
|
||||
- `HTTPClient` speaks the same REST endpoints used by the spike
|
||||
(`/api/v3/directory/check`, `/object`, `/relation`, `/manifest`).
|
||||
- A future gRPC client can replace `HTTPClient` without changing
|
||||
protected-system contracts or CARING envelope behavior.
|
||||
- Embedded Topaz remains out of scope because it would couple
|
||||
flex-auth releases to Topaz internals.
|
||||
|
||||
## Startup
|
||||
|
||||
1. Start Topaz with the manifest and bundle paths mounted. The
|
||||
`examples/topaz/docker-compose.yml` file is the local reference.
|
||||
2. Create a `topaz.HTTPClient` with the directory REST gateway URL.
|
||||
3. Configure a `topaz.FileBundleSink` that points at the mounted bundle
|
||||
directory.
|
||||
4. Build a `topaz.Adapter` with the client and policy metadata.
|
||||
5. Call `ImportManifest`, `ImportDirectory`, and `ImportPolicy` before
|
||||
accepting delegated checks.
|
||||
|
||||
For local verification:
|
||||
|
||||
```sh
|
||||
cd examples/topaz
|
||||
docker compose up --abort-on-container-exit --exit-code-from probe
|
||||
```
|
||||
|
||||
The Go integration test is present but skipped by default. Run it with:
|
||||
|
||||
```sh
|
||||
FLEX_AUTH_RUN_TOPAZ_INTEGRATION=1 go test ./internal/adapters/topaz
|
||||
```
|
||||
|
||||
## Directory Consistency
|
||||
|
||||
`ImportDirectory` converts the canonical registry snapshot into Topaz
|
||||
objects and relations. Subjects and service accounts become Topaz
|
||||
`user` objects. Each subject also receives an `identity:<subject>` object
|
||||
with an `identifier` relation to the user. Groups and teams become Topaz
|
||||
`group` objects; teams keep the `team:` prefix. Resources keep their
|
||||
canonical type names, labels, trust zone, path, owner, and system in
|
||||
properties.
|
||||
|
||||
Relation writes return an optional etag. The adapter records the latest
|
||||
etag in `DecisionEnvelope.provenance.directory_etag` when Topaz returns
|
||||
one. Reads may be served from flex-auth's local registry for explanation
|
||||
or from Topaz for authorization. The decision envelope must say which
|
||||
backend produced the answer through `provenance.evaluator` and
|
||||
`provenance.mode`.
|
||||
|
||||
## Policy Import
|
||||
|
||||
`ImportPolicy` extracts the validated Rego module from a
|
||||
Rego-in-Markdown package without translation. `FileBundleSink` writes:
|
||||
|
||||
- `.manifest` with the package root.
|
||||
- `policy/<package/path>.rego` with the exact extracted module.
|
||||
|
||||
This matches the local-bundle mode used by the Topaz example. Clustered
|
||||
Topaz deployments can replace the bundle sink with a remote bundle
|
||||
publisher without changing the adapter contract.
|
||||
|
||||
## Fail-Closed Defaults
|
||||
|
||||
Delegated checks do not leak backend errors to protected systems as
|
||||
ambiguous success. The adapter returns a deny envelope for:
|
||||
|
||||
- Topaz unavailable: `reason=topaz_unavailable`.
|
||||
- Stale directory: `reason=topaz_directory_stale`.
|
||||
- Partial result: `reason=topaz_partial_result`.
|
||||
- Untranslatable request: `reason=topaz_request_incomplete`.
|
||||
|
||||
Each failure includes a CARING conformance finding and
|
||||
`diagnostics.topaz_failure`. This keeps delegated mode behavior
|
||||
compatible with standalone fail-closed decisions and makes backend
|
||||
health visible to audits.
|
||||
|
||||
## CARING Preservation
|
||||
|
||||
The adapter preserves CARING descriptors from the request or backend
|
||||
result. It copies descriptor, restrictions, exposure modes, derived
|
||||
capabilities, conformance findings, and exposure-event hooks into the
|
||||
decision envelope. If no descriptor is available, the decision still
|
||||
contains a `TOPAZ-CARING-DESCRIPTOR-MISSING` warning so conformance
|
||||
checks can distinguish an authorization deny from a metadata gap.
|
||||
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.
|
||||
@@ -1,10 +1,10 @@
|
||||
# Flex-Auth Workplan Planning Map
|
||||
|
||||
Date: 2026-05-04
|
||||
Date: 2026-06-23
|
||||
|
||||
## Purpose
|
||||
|
||||
This document captures the initial sequencing view for flex-auth workplans.
|
||||
This document captures the current sequencing view for flex-auth workplans.
|
||||
|
||||
## Priority Scale
|
||||
|
||||
@@ -20,27 +20,66 @@ This document captures the initial sequencing view for flex-auth workplans.
|
||||
| Workplan | Priority | Status | Depends On | Current View |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `FLEX-WP-0001` | complete | done | none | Repo intent, boundaries, and authorization landscape research are complete. |
|
||||
| `FLEX-WP-0002` | P0 | todo | `FLEX-WP-0001` | Standalone policy-as-code core: schemas, local registry, policy packages, check APIs, explanations, decision log, CLI/service skeleton, tests. |
|
||||
| `FLEX-WP-0003` | P1 | todo | `FLEX-WP-0002` | Markitect consumer integration: resource namespace, manifest import, action vocabulary, decision fixtures, integration docs. |
|
||||
| `FLEX-WP-0004` | P2 | todo | `FLEX-WP-0002` | Delegated PDP and directory adapters: Topaz, OpenFGA/SpiceDB, OPA/Cedar, Keycloak Authorization Services, Entra/Graph/SCIM. |
|
||||
| `FLEX-WP-0005` | complete | done | `FLEX-WP-0001` | Foundations and Topaz alignment are complete: ADR-001/002/003, Go skeleton, `FlexAuthResourceManifest` schema pin, Topaz mapping spike, IAM Profile citation, ops-warden boundary clarification. |
|
||||
| `FLEX-WP-0002` | complete | completed | `FLEX-WP-0001`, `FLEX-WP-0005` | Standalone policy-as-code core is complete: schemas, local registry, CARING profile/descriptors, Rego-in-Markdown policy packages, check APIs, explanations, decision log, CLI/service skeleton, tests. |
|
||||
| `FLEX-WP-0003` | complete | completed | `FLEX-WP-0002` | Markitect consumer integration and first CARING benchmark are complete: resource namespace, manifest import, action vocabulary, descriptor fixtures, decision fixtures, integration docs. |
|
||||
| `FLEX-WP-0004` | complete | completed | `FLEX-WP-0002`, `FLEX-WP-0005` | Delegated PDP and directory adapter boundary work is complete: Topaz adapter shape, OpenFGA/SpiceDB, OPA/Cedar, Keycloak Authorization Services, Entra/Graph/SCIM, CARING envelope preservation. |
|
||||
| `FLEX-WP-0006` | complete | finished | `FLEX-WP-0002`, `FLEX-WP-0005` | Ops-warden unblocker is complete: flex-auth publishes `ssh-certificate` / `sign` policies, fixtures, and `/v1/check` smoke evidence for the opt-in pre-sign gate shipped in ops-warden `WARDEN-WP-0007` and tracked for production in `WARDEN-WP-0009`. |
|
||||
| `FLEX-WP-0007` | `P0` | blocked | `FLEX-WP-0006` | Repo-side production registry fixture, sync contract, runtime command, healthz coverage, and real actor/IAM tests are implemented. Operator deployment and OpenBao smoke remain blocked on reachable runtime selection and scoped VAULT_TOKEN refresh. |
|
||||
|
||||
## Dependency Notes
|
||||
|
||||
`FLEX-WP-0002` should come first because the protected-system-facing API must
|
||||
be stable before flex-auth delegates decisions to external engines.
|
||||
`FLEX-WP-0005` is inserted between `0001` and `0002` per the
|
||||
pre-implementation assessment in `docs/pre-implementation-assessment.md`.
|
||||
It pulls forward the decisions the original `0002` left implicit (language,
|
||||
policy format, evaluator alignment) and runs the Topaz mapping spike
|
||||
before the core's schemas and check API are written.
|
||||
|
||||
`FLEX-WP-0003` follows the core and uses Markitect as the first concrete
|
||||
consumer. Markitect has already completed its side of the initial contract in
|
||||
`MKTT-WP-0014`, but flex-auth must still implement the service-side registry
|
||||
and decision behavior.
|
||||
`docs/caring-architecture-blueprint.md` adds the 2026-05-17 CARING
|
||||
refinement: CARING remains the semantic standard, while flex-auth becomes
|
||||
the practical reference implementation for descriptors, conformance
|
||||
findings, decision metadata, explain output, and exposure-event audit
|
||||
records. This refinement changes the shape of `FLEX-WP-0002` but does not
|
||||
add a new predecessor workplan.
|
||||
|
||||
`FLEX-WP-0004` should wait for the standalone core so delegated engines do not
|
||||
define the whole architecture accidentally.
|
||||
`FLEX-WP-0002` comes after `0005` so the standalone evaluator embeds the
|
||||
OPA Rego library and produces decision envelopes shaped to match the
|
||||
delegated-mode envelopes added later. It now also pins the executable
|
||||
CARING profile in the same schema slice.
|
||||
|
||||
`FLEX-WP-0003` follows the core. Markitect has already completed its
|
||||
side of the contract in `MKTT-WP-0014`; flex-auth pins the manifest in
|
||||
`FLEX-WP-0005 T03` and implements the service-side registry and decision
|
||||
behavior in `0003`.
|
||||
It also becomes the first consumer benchmark for proving local roles and
|
||||
resource semantics can map cleanly into CARING dimensions.
|
||||
|
||||
`FLEX-WP-0004` waits for the standalone core for the same reason as
|
||||
before, but its Topaz evaluation task moved to `0005 T04`; this workplan
|
||||
now implements the Topaz adapter against the spike's output.
|
||||
Delegated adapters must preserve flex-auth's CARING descriptor and
|
||||
conformance fields even when backend-native role semantics differ.
|
||||
|
||||
`FLEX-WP-0006` was the cross-repo integration unblocker for
|
||||
ops-warden. ops-warden already implements the opt-in policy call
|
||||
(`policy.enabled: true`) and production OpenBao signing works without the
|
||||
gate. flex-auth now publishes the protected-system manifest,
|
||||
`ssh-certificate` / `sign` policy package, allow/deny fixtures, and
|
||||
`POST /v1/check` evidence that ops-warden can use before enabling
|
||||
`policy.enabled` in production.
|
||||
|
||||
## State Hub Mirror
|
||||
|
||||
Native State Hub dependency edges should mirror:
|
||||
Native State Hub dependency edges:
|
||||
|
||||
- `FLEX-WP-0002 -> FLEX-WP-0001`
|
||||
- `FLEX-WP-0005 -> FLEX-WP-0001`
|
||||
- `FLEX-WP-0002 -> FLEX-WP-0005`
|
||||
- `FLEX-WP-0002 -> FLEX-WP-0001` (preserved)
|
||||
- `FLEX-WP-0003 -> FLEX-WP-0002`
|
||||
- `FLEX-WP-0004 -> FLEX-WP-0002`
|
||||
- `FLEX-WP-0004 -> FLEX-WP-0005` (Topaz adapter consumes the spike)
|
||||
- `FLEX-WP-0006 -> FLEX-WP-0002`
|
||||
- `FLEX-WP-0006 -> FLEX-WP-0005`
|
||||
- ops-warden: `WARDEN-WP-0009` finished (caller + registry smoke). Production
|
||||
`policy.enabled: true` waits for `FLEX-WP-0007` (reachable flex-auth runtime).
|
||||
- `FLEX-WP-0007 -> FLEX-WP-0006`
|
||||
|
||||
22
examples/README.md
Normal file
22
examples/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# examples/
|
||||
|
||||
Runnable examples used both as documentation and as test fixtures.
|
||||
|
||||
Expected layout (filled in across FLEX-WP-0002 / FLEX-WP-0003 /
|
||||
FLEX-WP-0005):
|
||||
|
||||
```text
|
||||
examples/
|
||||
claims/ # key-cape lightweight-mode and Keycloak heavy-mode
|
||||
# claim envelopes (P5.5)
|
||||
caring/ # executable CARING descriptor, request,
|
||||
# decision, registry, and audit fixtures (P2.1)
|
||||
markitect/ # FlexAuthResourceManifest fixtures, decision
|
||||
# fixtures, and Rego-in-Markdown policy packages
|
||||
ops-warden/ # SSH certificate signing policy-gate fixtures
|
||||
# for ops-warden policy.enabled smoke checks
|
||||
topaz/ # docker-compose + sample directory and policy
|
||||
# for the Topaz alignment spike (P5.4)
|
||||
policies/ # generic Rego-in-Markdown packages used by
|
||||
# the standalone core tests
|
||||
```
|
||||
12
examples/caring/README.md
Normal file
12
examples/caring/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# CARING examples
|
||||
|
||||
Small fixtures for the executable CARING 0.4.0-RC2 profile used by
|
||||
`FLEX-WP-0002`.
|
||||
|
||||
These are intentionally compact. They prove that the canonical descriptor,
|
||||
request, decision, registry, audit, and Rego-in-Markdown policy package
|
||||
shapes can round-trip through `pkg/api` and `internal/policy`.
|
||||
|
||||
The set includes local subjects, groups, teams, project resources, inherited
|
||||
relationship facts, exposure events, allow/deny fixtures, and a
|
||||
redact-with-obligation policy package.
|
||||
26
examples/caring/access_descriptor.yaml
Normal file
26
examples/caring/access_descriptor.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
id: descriptor:tenant-alpha-document-reader
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Doer
|
||||
scope:
|
||||
level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
resource: document:internal-note
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
conditions:
|
||||
- PurposeBound
|
||||
- Logged
|
||||
lifecycle_state: Operate
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
access_path: direct
|
||||
metadata:
|
||||
source: examples/caring
|
||||
22
examples/caring/audit_event.json
Normal file
22
examples/caring/audit_event.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "audit:decision:tenant-alpha-internal-note",
|
||||
"type": "decision",
|
||||
"decision_id": "decision:tenant-alpha-internal-note",
|
||||
"subject": {
|
||||
"id": "user:alice",
|
||||
"type": "Human",
|
||||
"tenant": "tenant:alpha"
|
||||
},
|
||||
"resource": {
|
||||
"id": "document:internal-note",
|
||||
"type": "document",
|
||||
"system": "markitect-tool",
|
||||
"tenant": "tenant:alpha"
|
||||
},
|
||||
"action": "read",
|
||||
"effect": "allow",
|
||||
"timestamp": "2026-05-17T00:00:00Z",
|
||||
"metadata": {
|
||||
"profile": "caring-0.4.0-rc2"
|
||||
}
|
||||
}
|
||||
12
examples/caring/batch_check_request.yaml
Normal file
12
examples/caring/batch_check_request.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
id: batch:tenant-alpha-documents
|
||||
subject:
|
||||
id: user:alice
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
action: read
|
||||
resources:
|
||||
- id: document:internal-note
|
||||
system: markitect-tool
|
||||
- id: document:missing
|
||||
type: document
|
||||
system: markitect-tool
|
||||
41
examples/caring/check_request.yaml
Normal file
41
examples/caring/check_request.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
id: check:tenant-alpha-internal-note
|
||||
subject:
|
||||
id: user:alice
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
action: read
|
||||
resource:
|
||||
id: document:internal-note
|
||||
type: document
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
context:
|
||||
purpose: knowledge-base-read
|
||||
assurance:
|
||||
mfa: true
|
||||
caring_context:
|
||||
id: descriptor:tenant-alpha-document-reader
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Doer
|
||||
scope:
|
||||
level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
resource: document:internal-note
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
conditions:
|
||||
- PurposeBound
|
||||
- Logged
|
||||
lifecycle_state: Operate
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
access_path: direct
|
||||
policy_version: markitect.documents.v1
|
||||
69
examples/caring/decision_envelope.json
Normal file
69
examples/caring/decision_envelope.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"id": "decision:tenant-alpha-internal-note",
|
||||
"request_id": "check:tenant-alpha-internal-note",
|
||||
"effect": "allow",
|
||||
"reason": "reader_relation",
|
||||
"matched_policy_version": "markitect.documents.v1",
|
||||
"matched_rule": "allow_document_read",
|
||||
"resource": {
|
||||
"id": "document:internal-note",
|
||||
"type": "document",
|
||||
"system": "markitect-tool",
|
||||
"tenant": "tenant:alpha"
|
||||
},
|
||||
"subject": {
|
||||
"id": "user:alice",
|
||||
"type": "Human",
|
||||
"tenant": "tenant:alpha"
|
||||
},
|
||||
"obligations": [
|
||||
{
|
||||
"type": "log_access",
|
||||
"parameters": {
|
||||
"level": "standard"
|
||||
}
|
||||
}
|
||||
],
|
||||
"diagnostics": {
|
||||
"policy_package": "examples/caring"
|
||||
},
|
||||
"provenance": {
|
||||
"evaluator": "flex-auth",
|
||||
"mode": "standalone",
|
||||
"policy_package": "markitect.documents",
|
||||
"policy_version": "v1",
|
||||
"decision_time": "2026-05-17T00:00:00Z"
|
||||
},
|
||||
"caring": {
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"descriptor": {
|
||||
"id": "descriptor:tenant-alpha-document-reader",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Human",
|
||||
"organization_relation": "Customer",
|
||||
"canonical_role": "Doer",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "document:internal-note",
|
||||
"tenant": "tenant:alpha",
|
||||
"resource": "document:internal-note"
|
||||
},
|
||||
"planes": ["Data"],
|
||||
"capabilities": ["View"],
|
||||
"exposure_modes": ["Masked", "Plaintext"],
|
||||
"conditions": ["PurposeBound", "Logged"],
|
||||
"lifecycle_state": "Operate",
|
||||
"restrictions": ["ExportBlocked"],
|
||||
"access_path": "direct"
|
||||
},
|
||||
"restrictions_evaluated": ["ExportBlocked"],
|
||||
"exposure_modes": ["Masked", "Plaintext"],
|
||||
"conformance_findings": [
|
||||
{
|
||||
"code": "CARING-EXPORT-SEPARATION",
|
||||
"severity": "info",
|
||||
"message": "View is allowed, but Exportable exposure remains separately blocked."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
20
examples/caring/exposure_event.json
Normal file
20
examples/caring/exposure_event.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "exposure:tenant-alpha-support-001",
|
||||
"type": "X-Support",
|
||||
"actor": "user:alice",
|
||||
"subject": "user:bob",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "document:alpha-plan",
|
||||
"tenant": "tenant:alpha",
|
||||
"resource": "document:alpha-plan"
|
||||
},
|
||||
"planes": ["Data"],
|
||||
"exposure_modes": ["Masked"],
|
||||
"reason": "Support review of masked project plan",
|
||||
"decision_id": "decision:tenant-alpha-support-001",
|
||||
"timestamp": "2026-05-17T00:00:00Z",
|
||||
"metadata": {
|
||||
"source": "examples/caring/exposure_event.json"
|
||||
}
|
||||
}
|
||||
38
examples/caring/inherited_relationships.yaml
Normal file
38
examples/caring/inherited_relationships.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
- id: rel:reviewers-project-reviewer
|
||||
system: markitect-tool
|
||||
subject: team:project-reviewers
|
||||
relation: reviewer
|
||||
object: project:alpha-redesign
|
||||
tenant: tenant:alpha
|
||||
conditions:
|
||||
- Logged
|
||||
caring:
|
||||
id: descriptor:tenant-alpha-project-reviewer
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Group
|
||||
organization_relation: Customer
|
||||
canonical_role: Verifier
|
||||
scope:
|
||||
level: Project
|
||||
id: project:alpha-redesign
|
||||
tenant: tenant:alpha
|
||||
resource: project:alpha-redesign
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- Review
|
||||
exposure_modes:
|
||||
- Masked
|
||||
conditions:
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
- id: rel:alpha-plan-inherits-project-reviewer
|
||||
system: markitect-tool
|
||||
subject: document:alpha-plan
|
||||
relation: inherits
|
||||
object: project:alpha-redesign
|
||||
tenant: tenant:alpha
|
||||
metadata:
|
||||
inheritance: parent
|
||||
source: examples/caring/inherited_relationships.yaml
|
||||
45
examples/caring/policy_fixture.yaml
Normal file
45
examples/caring/policy_fixture.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
id: fixture:markitect-internal-read-allow
|
||||
request:
|
||||
id: check:tenant-alpha-internal-note
|
||||
subject:
|
||||
id: user:alice
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
action: read
|
||||
resource:
|
||||
id: document:internal-note
|
||||
type: document
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
caring_context:
|
||||
id: descriptor:tenant-alpha-document-reader
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Doer
|
||||
scope:
|
||||
level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
resource: document:internal-note
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
conditions:
|
||||
- PurposeBound
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
expect:
|
||||
effect: allow
|
||||
reason: reader_relation
|
||||
conformance_findings:
|
||||
- code: CARING-EXPORT-SEPARATION
|
||||
severity: info
|
||||
message: View is allowed, but Exportable exposure remains separately blocked.
|
||||
metadata:
|
||||
source: examples/caring
|
||||
137
examples/caring/policy_package.md
Normal file
137
examples/caring/policy_package.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
id: markitect.documents.internal-read
|
||||
name: Markitect internal document read
|
||||
namespace: markitect:document
|
||||
version: v1
|
||||
status: draft
|
||||
package: flexauth.markitect.documents
|
||||
actions:
|
||||
- read
|
||||
owner: team:platform-architecture
|
||||
fixtures:
|
||||
- policy_fixture.yaml
|
||||
caring:
|
||||
profile: caring-0.4.0-rc2
|
||||
enforce: false
|
||||
canonical_roles:
|
||||
- Doer
|
||||
organization_relations:
|
||||
- Customer
|
||||
scopes:
|
||||
- level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
conditions:
|
||||
- PurposeBound
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
activation:
|
||||
mode: local
|
||||
metadata:
|
||||
source: examples/caring/policy_package.md
|
||||
---
|
||||
|
||||
# Markitect Internal Document Read
|
||||
|
||||
This package authorizes read access to an internal Markitect document when
|
||||
the request carries a CARING descriptor for a customer Doer with View
|
||||
capability on the document resource and an explicit ExportBlocked restriction.
|
||||
|
||||
## Rules
|
||||
|
||||
```rego
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
default decision := {"effect": "deny", "reason": "no_matching_rule"}
|
||||
|
||||
decision := {
|
||||
"effect": "allow",
|
||||
"reason": "reader_relation",
|
||||
"conformance_findings": [{
|
||||
"code": "CARING-EXPORT-SEPARATION",
|
||||
"severity": "info",
|
||||
"message": "View is allowed, but Exportable exposure remains separately blocked."
|
||||
}]
|
||||
} if {
|
||||
input.action == "read"
|
||||
input.resource.system == "markitect-tool"
|
||||
input.resource.type == "document"
|
||||
input.caring_context.profile == "caring-0.4.0-rc2"
|
||||
input.caring_context.organization_relation == "Customer"
|
||||
input.caring_context.canonical_role == "Doer"
|
||||
"View" in input.caring_context.capabilities
|
||||
"ExportBlocked" in input.caring_context.restrictions
|
||||
}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```rego test
|
||||
package flexauth.markitect.documents_test
|
||||
|
||||
import future.keywords.if
|
||||
import data.flexauth.markitect.documents
|
||||
|
||||
test_reader_relation_allows if {
|
||||
documents.decision.effect == "allow" with input as {
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"id": "document:internal-note",
|
||||
"type": "document",
|
||||
"system": "markitect-tool",
|
||||
"tenant": "tenant:alpha"
|
||||
},
|
||||
"caring_context": {
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"organization_relation": "Customer",
|
||||
"canonical_role": "Doer",
|
||||
"capabilities": ["View"],
|
||||
"restrictions": ["ExportBlocked"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_missing_caring_context_denies if {
|
||||
documents.decision.effect == "deny" with input as {
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"id": "document:internal-note",
|
||||
"type": "document",
|
||||
"system": "markitect-tool",
|
||||
"tenant": "tenant:alpha"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
```yaml fixture
|
||||
id: fixture:markitect-internal-read-deny
|
||||
request:
|
||||
id: check:tenant-alpha-internal-note-deny
|
||||
subject:
|
||||
id: user:bob
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
action: read
|
||||
resource:
|
||||
id: document:internal-note
|
||||
type: document
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
expect:
|
||||
effect: deny
|
||||
reason: no_matching_rule
|
||||
metadata:
|
||||
source: examples/caring/policy_package.md
|
||||
```
|
||||
29
examples/caring/policy_package.yaml
Normal file
29
examples/caring/policy_package.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
id: markitect.documents.internal-read
|
||||
name: Markitect internal document read
|
||||
version: v1
|
||||
status: draft
|
||||
package: flexauth.markitect.documents
|
||||
caring:
|
||||
profile: caring-0.4.0-rc2
|
||||
canonical_roles:
|
||||
- Doer
|
||||
organization_relations:
|
||||
- Customer
|
||||
scopes:
|
||||
- level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
conditions:
|
||||
- PurposeBound
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
metadata:
|
||||
source: examples/caring
|
||||
27
examples/caring/project_resource_manifest.yaml
Normal file
27
examples/caring/project_resource_manifest.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
id: markitect-project-resources
|
||||
system: markitect-tool
|
||||
resources:
|
||||
- id: project:alpha-redesign
|
||||
type: project
|
||||
path: /projects/alpha-redesign
|
||||
labels:
|
||||
- project
|
||||
trust_zone: internal
|
||||
owner: team:project-reviewers
|
||||
- id: document:alpha-plan
|
||||
type: document
|
||||
path: /projects/alpha-redesign/plan
|
||||
parent: project:alpha-redesign
|
||||
labels:
|
||||
- internal
|
||||
- pii
|
||||
trust_zone: internal
|
||||
owner: team:project-reviewers
|
||||
actions:
|
||||
- read
|
||||
- review
|
||||
- export
|
||||
caring_profile: caring-0.4.0-rc2
|
||||
metadata:
|
||||
flex_auth_contract: resource-registration-v0
|
||||
source: examples/caring/project_resource_manifest.yaml
|
||||
132
examples/caring/redact_policy_package.md
Normal file
132
examples/caring/redact_policy_package.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
id: markitect.documents.mask-pii
|
||||
name: Markitect masked PII read
|
||||
namespace: markitect:document
|
||||
version: v1
|
||||
status: draft
|
||||
package: flexauth.markitect.redact
|
||||
actions:
|
||||
- read
|
||||
owner: team:project-reviewers
|
||||
caring:
|
||||
profile: caring-0.4.0-rc2
|
||||
enforce: false
|
||||
canonical_roles:
|
||||
- Verifier
|
||||
organization_relations:
|
||||
- Customer
|
||||
scopes:
|
||||
- level: Resource
|
||||
id: document:alpha-plan
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
- Mask
|
||||
exposure_modes:
|
||||
- Masked
|
||||
conditions:
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
metadata:
|
||||
source: examples/caring/redact_policy_package.md
|
||||
---
|
||||
|
||||
# Markitect Masked PII Read
|
||||
|
||||
This package returns a redaction decision when a verifier may inspect a
|
||||
document only through masked fields.
|
||||
|
||||
## Rules
|
||||
|
||||
```rego
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
default decision := {"effect": "deny", "reason": "no_matching_rule"}
|
||||
|
||||
decision := {
|
||||
"effect": "redact",
|
||||
"reason": "masked_pii",
|
||||
"obligations": [{
|
||||
"type": "mask_fields",
|
||||
"parameters": {"fields": ["email", "phone"]}
|
||||
}]
|
||||
} if {
|
||||
input.action == "read"
|
||||
input.resource.id == "document:alpha-plan"
|
||||
"Mask" in input.caring_context.capabilities
|
||||
"Masked" in input.caring_context.exposure_modes
|
||||
}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```rego test
|
||||
package flexauth.markitect.redact_test
|
||||
|
||||
import future.keywords.if
|
||||
import data.flexauth.markitect.redact
|
||||
|
||||
test_masked_reader_gets_redaction if {
|
||||
redact.decision.effect == "redact" with input as {
|
||||
"action": "read",
|
||||
"resource": {"id": "document:alpha-plan"},
|
||||
"caring_context": {
|
||||
"capabilities": ["View", "Mask"],
|
||||
"exposure_modes": ["Masked"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
```yaml fixture
|
||||
id: fixture:masked-pii-redact
|
||||
request:
|
||||
id: check:masked-pii
|
||||
subject:
|
||||
id: user:bob
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
action: read
|
||||
resource:
|
||||
id: document:alpha-plan
|
||||
type: document
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
caring_context:
|
||||
id: descriptor:tenant-alpha-masked-pii-reviewer
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Verifier
|
||||
scope:
|
||||
level: Resource
|
||||
id: document:alpha-plan
|
||||
tenant: tenant:alpha
|
||||
resource: document:alpha-plan
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
- Mask
|
||||
exposure_modes:
|
||||
- Masked
|
||||
conditions:
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
expect:
|
||||
effect: redact
|
||||
reason: masked_pii
|
||||
obligations:
|
||||
- type: mask_fields
|
||||
parameters:
|
||||
fields:
|
||||
- email
|
||||
- phone
|
||||
```
|
||||
99
examples/caring/registry_snapshot.json
Normal file
99
examples/caring/registry_snapshot.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"systems": [
|
||||
{
|
||||
"id": "markitect-tool",
|
||||
"name": "Markitect Tool",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": "document",
|
||||
"scope_level": "Resource",
|
||||
"planes": ["Data"]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "read",
|
||||
"capabilities": ["View"],
|
||||
"planes": ["Data"],
|
||||
"exposure_modes": ["Masked", "Plaintext"]
|
||||
}
|
||||
],
|
||||
"caring_profiles": ["caring-0.4.0-rc2"]
|
||||
}
|
||||
],
|
||||
"resource_manifests": [
|
||||
{
|
||||
"id": "markitect-example-knowledge-base",
|
||||
"system": "markitect-tool",
|
||||
"resources": [
|
||||
{
|
||||
"id": "document:internal-note",
|
||||
"type": "document",
|
||||
"parent": "knowledge-base:markitect-example",
|
||||
"labels": ["internal"],
|
||||
"trust_zone": "internal",
|
||||
"owner": "team:platform"
|
||||
}
|
||||
],
|
||||
"actions": ["read", "query", "search", "package", "export"],
|
||||
"caring_profile": "caring-0.4.0-rc2",
|
||||
"metadata": {
|
||||
"flex_auth_contract": "resource-registration-v0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tenants": [
|
||||
{
|
||||
"id": "tenant:alpha",
|
||||
"name": "Tenant Alpha"
|
||||
}
|
||||
],
|
||||
"subjects": [
|
||||
{
|
||||
"id": "user:alice",
|
||||
"type": "Human",
|
||||
"display_name": "Alice Example",
|
||||
"organization_relation": "Customer",
|
||||
"roles": ["Doer"],
|
||||
"groups": ["group:platform-architecture"],
|
||||
"tenant": "tenant:alpha"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "group:platform-architecture",
|
||||
"display_name": "Platform Architecture",
|
||||
"members": ["user:alice"],
|
||||
"tenant": "tenant:alpha"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"id": "rel:alice-reader-internal-note",
|
||||
"system": "markitect-tool",
|
||||
"subject": "group:platform-architecture",
|
||||
"relation": "reader",
|
||||
"object": "document:internal-note",
|
||||
"tenant": "tenant:alpha",
|
||||
"conditions": ["Logged"],
|
||||
"caring": {
|
||||
"id": "descriptor:tenant-alpha-document-reader",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "Customer",
|
||||
"canonical_role": "Doer",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "document:internal-note",
|
||||
"tenant": "tenant:alpha",
|
||||
"resource": "document:internal-note"
|
||||
},
|
||||
"planes": ["Data"],
|
||||
"capabilities": ["View"],
|
||||
"exposure_modes": ["Masked", "Plaintext"],
|
||||
"conditions": ["Logged"],
|
||||
"restrictions": ["ExportBlocked"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
32
examples/caring/relationship_fact.yaml
Normal file
32
examples/caring/relationship_fact.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
id: rel:alice-reader-internal-note
|
||||
system: markitect-tool
|
||||
subject: group:platform-architecture
|
||||
relation: reader
|
||||
object: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
conditions:
|
||||
- Logged
|
||||
caring:
|
||||
id: descriptor:tenant-alpha-document-reader
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Group
|
||||
organization_relation: Customer
|
||||
canonical_role: Doer
|
||||
scope:
|
||||
level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
resource: document:internal-note
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
conditions:
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
provenance:
|
||||
source: examples/caring
|
||||
22
examples/caring/subject_manifest.yaml
Normal file
22
examples/caring/subject_manifest.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
id: subjects:tenant-alpha
|
||||
subjects:
|
||||
- id: user:alice
|
||||
type: Human
|
||||
display_name: Alice Example
|
||||
organization_relation: Customer
|
||||
roles:
|
||||
- Doer
|
||||
groups:
|
||||
- group:platform-architecture
|
||||
tenant: tenant:alpha
|
||||
groups:
|
||||
- id: group:platform-architecture
|
||||
display_name: Platform Architecture
|
||||
members:
|
||||
- user:alice
|
||||
tenant: tenant:alpha
|
||||
tenants:
|
||||
- id: tenant:alpha
|
||||
name: Tenant Alpha
|
||||
metadata:
|
||||
source: examples/caring
|
||||
37
examples/caring/team_subject_manifest.yaml
Normal file
37
examples/caring/team_subject_manifest.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
id: tenant-alpha-project-team
|
||||
tenants:
|
||||
- id: tenant:alpha
|
||||
name: Tenant Alpha
|
||||
subjects:
|
||||
- id: user:alice
|
||||
type: Human
|
||||
display_name: Alice Example
|
||||
organization_relation: Customer
|
||||
roles:
|
||||
- Doer
|
||||
groups:
|
||||
- group:platform-architecture
|
||||
tenant: tenant:alpha
|
||||
- id: user:bob
|
||||
type: Human
|
||||
display_name: Bob Example
|
||||
organization_relation: Customer
|
||||
roles:
|
||||
- Verifier
|
||||
groups:
|
||||
- team:project-reviewers
|
||||
tenant: tenant:alpha
|
||||
groups:
|
||||
- id: group:platform-architecture
|
||||
display_name: Platform Architecture
|
||||
members:
|
||||
- user:alice
|
||||
tenant: tenant:alpha
|
||||
teams:
|
||||
- id: team:project-reviewers
|
||||
display_name: Project Reviewers
|
||||
members:
|
||||
- user:bob
|
||||
tenant: tenant:alpha
|
||||
metadata:
|
||||
source: examples/caring/team_subject_manifest.yaml
|
||||
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.2 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, explicit tenant/principal/assurance. |
|
||||
| `keycloak-heavy.yaml` | Keycloak production | Full variation set: canonical `roles`, provider-native role sources, scope as space-separated string, MFA assurance, multiple audiences. |
|
||||
| `service-account.yaml` | Either provider | Service account; `principal_type: service`, `service` + `operator` roles, no `preferred_username`, narrow scope. |
|
||||
| `emergency.yaml` | Either provider | Break-glass human identity; `emergency` role, `assurance.level: break_glass`, short expiry, 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.
|
||||
44
examples/claims/emergency.yaml
Normal file
44
examples/claims/emergency.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
# 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.2 "Emergency And Break-Glass
|
||||
# Access". flex-auth maps the emergency role plus break_glass assurance to
|
||||
# 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
|
||||
tenant: tenant:platform
|
||||
principal_type: human
|
||||
azp: ops-console
|
||||
preferred_username: ada
|
||||
email: ada@netkingdom.example
|
||||
scope: openid profile hub:admin
|
||||
roles:
|
||||
- emergency
|
||||
- admin
|
||||
groups:
|
||||
- /platform/stewards
|
||||
amr:
|
||||
- pwd
|
||||
- otp
|
||||
- hwk
|
||||
acr: "3"
|
||||
assurance:
|
||||
level: break_glass
|
||||
methods:
|
||||
- pwd
|
||||
- otp
|
||||
- hwk
|
||||
mfa: true
|
||||
source: keycloak
|
||||
at: 1767225595
|
||||
emergency:
|
||||
incident_id: INC-2026-0042
|
||||
authorized_by: "team:platform-stewards"
|
||||
reason: "credential rotation playbook step 4"
|
||||
33
examples/claims/key-cape-lightweight.yaml
Normal file
33
examples/claims/key-cape-lightweight.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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.2
|
||||
# "Core 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
|
||||
tenant: tenant:platform
|
||||
principal_type: human
|
||||
preferred_username: ada
|
||||
email: ada@netkingdom.local
|
||||
name: Ada Lovelace
|
||||
scope: openid profile hub:read
|
||||
roles:
|
||||
- viewer
|
||||
amr:
|
||||
- pwd
|
||||
acr: "1"
|
||||
groups:
|
||||
- /markitect/readers
|
||||
assurance:
|
||||
level: aal1
|
||||
methods:
|
||||
- pwd
|
||||
mfa: false
|
||||
source: key-cape
|
||||
at: 1767225600
|
||||
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
|
||||
55
examples/claims/keycloak-heavy.yaml
Normal file
55
examples/claims/keycloak-heavy.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
# 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
|
||||
tenant: tenant:platform
|
||||
principal_type: human
|
||||
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
|
||||
roles:
|
||||
- operator
|
||||
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"
|
||||
assurance:
|
||||
level: aal2
|
||||
methods:
|
||||
- pwd
|
||||
- otp
|
||||
mfa: true
|
||||
source: keycloak
|
||||
at: 1767225590
|
||||
sid: 4c0a3a8a-3a47-4f2f-8e89-9e5f9b0a0a0a
|
||||
29
examples/claims/service-account.yaml
Normal file
29
examples/claims/service-account.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
# 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.2 "Service Account Flow".
|
||||
|
||||
iss: https://sso.netkingdom.example/realms/netkingdom
|
||||
sub: svc-markitect-tool-prod
|
||||
aud:
|
||||
- flex-auth
|
||||
exp: 4102444800
|
||||
iat: 1767225600
|
||||
tenant: tenant:platform
|
||||
principal_type: service
|
||||
azp: svc-markitect-tool-prod
|
||||
client_id: svc-markitect-tool-prod
|
||||
scope: hub:read hub:capability
|
||||
roles:
|
||||
- service
|
||||
- operator
|
||||
groups: []
|
||||
assurance:
|
||||
level: aal1
|
||||
methods:
|
||||
- client_secret
|
||||
mfa: false
|
||||
source: keycloak
|
||||
at: 1767225600
|
||||
13
examples/markitect/ambiguous_resource_manifest.yaml
Normal file
13
examples/markitect/ambiguous_resource_manifest.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
id: markitect-ambiguous-example
|
||||
system: markitect-tool
|
||||
caring_profile: caring-0.4.0-rc2
|
||||
resources:
|
||||
- id: document:ambiguous-note
|
||||
type: document
|
||||
parent: knowledge-base:markitect-example
|
||||
path: examples/policy/ambiguous-note.md
|
||||
actions:
|
||||
- read
|
||||
metadata:
|
||||
source: examples/markitect/ambiguous_resource_manifest.yaml
|
||||
flex_auth_contract: resource-registration-v0
|
||||
239
examples/markitect/check_fixtures.yaml
Normal file
239
examples/markitect/check_fixtures.yaml
Normal file
@@ -0,0 +1,239 @@
|
||||
- id: fixture:markitect-public-document-allow
|
||||
request:
|
||||
id: check:markitect-public-document
|
||||
subject:
|
||||
id: user:visitor
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
action: read
|
||||
resource:
|
||||
id: document:public-note
|
||||
type: document
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
labels:
|
||||
- public
|
||||
trust_zone: public
|
||||
caring_context:
|
||||
id: descriptor:public-document-reader
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Doer
|
||||
scope:
|
||||
level: Resource
|
||||
id: document:public-note
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Plaintext
|
||||
conditions:
|
||||
- Logged
|
||||
expect:
|
||||
effect: allow
|
||||
reason: public_document
|
||||
metadata:
|
||||
expected_caring_descriptor: descriptor:public-document-reader
|
||||
expected_conformance_findings: []
|
||||
expected_exposure_modes:
|
||||
- Plaintext
|
||||
expected_audit_behavior: sampled_allow
|
||||
- id: fixture:markitect-internal-document-deny
|
||||
request:
|
||||
id: check:markitect-internal-document-deny
|
||||
subject:
|
||||
id: user:visitor
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
groups: []
|
||||
action: read
|
||||
resource:
|
||||
id: document:internal-note
|
||||
type: document
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
labels:
|
||||
- internal
|
||||
trust_zone: internal
|
||||
expect:
|
||||
effect: deny
|
||||
reason: no_matching_rule
|
||||
metadata:
|
||||
expected_caring_descriptor: null
|
||||
expected_conformance_findings: []
|
||||
expected_exposure_modes:
|
||||
- None
|
||||
expected_audit_behavior: always_record
|
||||
- id: fixture:markitect-internal-document-reader-allow
|
||||
request:
|
||||
id: check:markitect-internal-document-reader
|
||||
subject:
|
||||
id: user:alice
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
groups:
|
||||
- group:platform-architecture
|
||||
action: read
|
||||
resource:
|
||||
id: document:internal-note
|
||||
type: document
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
labels:
|
||||
- internal
|
||||
trust_zone: internal
|
||||
caring_context:
|
||||
id: descriptor:internal-document-reader
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Doer
|
||||
scope:
|
||||
level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Data
|
||||
capabilities:
|
||||
- View
|
||||
exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
conditions:
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
expect:
|
||||
effect: allow
|
||||
reason: reader_group
|
||||
metadata:
|
||||
expected_caring_descriptor: descriptor:internal-document-reader
|
||||
expected_conformance_findings: []
|
||||
expected_exposure_modes:
|
||||
- Masked
|
||||
- Plaintext
|
||||
expected_audit_behavior: sampled_allow
|
||||
- id: fixture:markitect-restricted-export-steward-mfa
|
||||
request:
|
||||
id: check:markitect-restricted-export
|
||||
subject:
|
||||
id: user:steward
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
roles:
|
||||
- steward
|
||||
action: export
|
||||
resource:
|
||||
id: export:internal-note-review-bundle
|
||||
type: export
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
labels:
|
||||
- export
|
||||
trust_zone: external
|
||||
context:
|
||||
mfa: true
|
||||
reason: customer-approved export
|
||||
caring_context:
|
||||
id: descriptor:restricted-export-steward
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Maintainer
|
||||
scope:
|
||||
level: Record
|
||||
id: export:internal-note-review-bundle
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Data
|
||||
- Audit
|
||||
capabilities:
|
||||
- Export
|
||||
exposure_modes:
|
||||
- Exportable
|
||||
- Plaintext
|
||||
conditions:
|
||||
- MFARequired
|
||||
- Logged
|
||||
expect:
|
||||
effect: allow
|
||||
reason: steward_export_mfa
|
||||
conformance_findings:
|
||||
- code: MARKITECT-EXPORT-MFA-LOGGED
|
||||
severity: info
|
||||
message: Export is allowed only with steward role, MFA, and logging.
|
||||
metadata:
|
||||
expected_caring_descriptor: descriptor:restricted-export-steward
|
||||
expected_exposure_modes:
|
||||
- Exportable
|
||||
- Plaintext
|
||||
expected_audit_behavior: always_record
|
||||
- id: fixture:markitect-context-package-activation
|
||||
request:
|
||||
id: check:markitect-context-package-activation
|
||||
subject:
|
||||
id: user:alice
|
||||
type: Human
|
||||
tenant: tenant:alpha
|
||||
action: activate_context
|
||||
resource:
|
||||
id: context-package:internal-note-review
|
||||
type: context_package
|
||||
system: markitect-tool
|
||||
tenant: tenant:alpha
|
||||
attributes:
|
||||
labels:
|
||||
- internal
|
||||
- generated
|
||||
context:
|
||||
freshness_seconds: 600
|
||||
policy_version: markitect-gateway-v1
|
||||
caring_context:
|
||||
id: descriptor:context-package-activation
|
||||
profile: caring-0.4.0-rc2
|
||||
subject_type: Human
|
||||
organization_relation: Customer
|
||||
canonical_role: Verifier
|
||||
scope:
|
||||
level: Dataset
|
||||
id: context-package:internal-note-review
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Intent
|
||||
- Policy
|
||||
capabilities:
|
||||
- Use
|
||||
- Execute
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Masked
|
||||
conditions:
|
||||
- PurposeBound
|
||||
- Logged
|
||||
expect:
|
||||
effect: allow
|
||||
reason: fresh_context_package
|
||||
obligations:
|
||||
- type: record_context_activation
|
||||
parameters:
|
||||
freshness_seconds: 600
|
||||
conformance_findings:
|
||||
- code: MARKITECT-CONTEXT-FRESHNESS
|
||||
severity: info
|
||||
message: Context package activation includes policy version and freshness metadata.
|
||||
metadata:
|
||||
expected_caring_descriptor: descriptor:context-package-activation
|
||||
expected_exposure_modes:
|
||||
- Metadata
|
||||
- Masked
|
||||
expected_audit_behavior: always_record
|
||||
152
examples/markitect/check_policy_package.md
Normal file
152
examples/markitect/check_policy_package.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
id: markitect.gateway.check-fixtures
|
||||
name: Markitect gateway check fixtures
|
||||
namespace: markitect:gateway
|
||||
version: v1
|
||||
status: draft
|
||||
package: flexauth.markitect.gateway
|
||||
actions:
|
||||
- read
|
||||
- export
|
||||
- activate_context
|
||||
owner: team:platform-architecture
|
||||
fixtures:
|
||||
- check_fixtures.yaml
|
||||
caring:
|
||||
profile: caring-0.4.0-rc2
|
||||
enforce: false
|
||||
canonical_roles:
|
||||
- Doer
|
||||
- Maintainer
|
||||
- Verifier
|
||||
organization_relations:
|
||||
- Customer
|
||||
scopes:
|
||||
- level: Resource
|
||||
id: document:public-note
|
||||
tenant: tenant:alpha
|
||||
- level: Resource
|
||||
id: document:internal-note
|
||||
tenant: tenant:alpha
|
||||
- level: Dataset
|
||||
id: context-package:internal-note-review
|
||||
tenant: tenant:alpha
|
||||
planes:
|
||||
- Intent
|
||||
- Data
|
||||
- Audit
|
||||
capabilities:
|
||||
- View
|
||||
- Export
|
||||
- Use
|
||||
- Execute
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Masked
|
||||
- Plaintext
|
||||
- Exportable
|
||||
conditions:
|
||||
- MFARequired
|
||||
- PurposeBound
|
||||
- Logged
|
||||
restrictions:
|
||||
- ExportBlocked
|
||||
metadata:
|
||||
source: examples/markitect/check_policy_package.md
|
||||
---
|
||||
|
||||
# Markitect Gateway Check Fixtures
|
||||
|
||||
This package captures the first Markitect gateway scenarios as executable Rego
|
||||
and external fixtures.
|
||||
|
||||
## Rules
|
||||
|
||||
```rego
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
default decision := {"effect": "deny", "reason": "no_matching_rule"}
|
||||
|
||||
decision := {"effect": "allow", "reason": "public_document"} if {
|
||||
input.action == "read"
|
||||
input.resource.type == "document"
|
||||
"public" in object.get(input.resource.attributes, "labels", [])
|
||||
}
|
||||
|
||||
decision := {"effect": "allow", "reason": "reader_group"} if {
|
||||
input.action == "read"
|
||||
input.resource.type == "document"
|
||||
"internal" in object.get(input.resource.attributes, "labels", [])
|
||||
"group:platform-architecture" in object.get(input.subject.attributes, "groups", [])
|
||||
"View" in input.caring_context.capabilities
|
||||
}
|
||||
|
||||
decision := {
|
||||
"effect": "allow",
|
||||
"reason": "steward_export_mfa",
|
||||
"conformance_findings": [{
|
||||
"code": "MARKITECT-EXPORT-MFA-LOGGED",
|
||||
"severity": "info",
|
||||
"message": "Export is allowed only with steward role, MFA, and logging."
|
||||
}]
|
||||
} if {
|
||||
input.action == "export"
|
||||
"steward" in object.get(input.subject.attributes, "roles", [])
|
||||
input.context.mfa == true
|
||||
"Export" in input.caring_context.capabilities
|
||||
"Exportable" in input.caring_context.exposure_modes
|
||||
}
|
||||
|
||||
decision := {
|
||||
"effect": "allow",
|
||||
"reason": "fresh_context_package",
|
||||
"obligations": [{
|
||||
"type": "record_context_activation",
|
||||
"parameters": {"freshness_seconds": input.context.freshness_seconds}
|
||||
}],
|
||||
"conformance_findings": [{
|
||||
"code": "MARKITECT-CONTEXT-FRESHNESS",
|
||||
"severity": "info",
|
||||
"message": "Context package activation includes policy version and freshness metadata."
|
||||
}]
|
||||
} if {
|
||||
input.action == "activate_context"
|
||||
input.resource.type == "context_package"
|
||||
input.policy_version != ""
|
||||
input.context.freshness_seconds <= 900
|
||||
"Use" in input.caring_context.capabilities
|
||||
"Execute" in input.caring_context.capabilities
|
||||
}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```rego test
|
||||
package flexauth.markitect.gateway_test
|
||||
|
||||
import future.keywords.if
|
||||
import data.flexauth.markitect.gateway
|
||||
|
||||
test_public_document_allows if {
|
||||
gateway.decision.effect == "allow" with input as {
|
||||
"action": "read",
|
||||
"resource": {
|
||||
"type": "document",
|
||||
"attributes": {"labels": ["public"]}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_export_requires_mfa if {
|
||||
gateway.decision.effect == "deny" with input as {
|
||||
"action": "export",
|
||||
"subject": {"attributes": {"roles": ["steward"]}},
|
||||
"context": {"mfa": false},
|
||||
"caring_context": {
|
||||
"capabilities": ["Export"],
|
||||
"exposure_modes": ["Exportable"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
83
examples/markitect/namespace_resource_manifest.yaml
Normal file
83
examples/markitect/namespace_resource_manifest.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
id: markitect-namespace-example
|
||||
system: markitect-tool
|
||||
caring_profile: caring-0.4.0-rc2
|
||||
resources:
|
||||
- id: knowledge-base:markitect-example
|
||||
type: knowledge_base
|
||||
labels:
|
||||
- internal
|
||||
trust_zone: internal
|
||||
owner: team:platform-architecture
|
||||
- id: repository:markitect-policy
|
||||
type: repository
|
||||
parent: knowledge-base:markitect-example
|
||||
path: repos/markitect-policy
|
||||
labels:
|
||||
- internal
|
||||
trust_zone: internal
|
||||
owner: team:platform-architecture
|
||||
- id: document:internal-note
|
||||
type: document
|
||||
parent: repository:markitect-policy
|
||||
path: examples/policy/private/internal-note.md
|
||||
labels:
|
||||
- internal
|
||||
- pii
|
||||
trust_zone: internal
|
||||
owner: team:platform-architecture
|
||||
attributes:
|
||||
markitect_path: examples/policy/private/internal-note.md
|
||||
frontmatter_visibility: internal
|
||||
source_revision: rev:example
|
||||
- id: section:internal-note#risk
|
||||
type: section
|
||||
parent: document:internal-note
|
||||
path: examples/policy/private/internal-note.md#risk
|
||||
labels:
|
||||
- internal
|
||||
trust_zone: internal
|
||||
- id: span:internal-note#risk:customer-email
|
||||
type: span
|
||||
parent: section:internal-note#risk
|
||||
labels:
|
||||
- pii
|
||||
trust_zone: restricted
|
||||
attributes:
|
||||
data_classes:
|
||||
- email
|
||||
- id: context-package:internal-note-review
|
||||
type: context_package
|
||||
parent: document:internal-note
|
||||
labels:
|
||||
- internal
|
||||
- generated
|
||||
trust_zone: internal
|
||||
attributes:
|
||||
freshness_seconds: 900
|
||||
workflow_state: prepared
|
||||
- id: workflow-artifact:internal-note-review-run
|
||||
type: workflow_artifact
|
||||
parent: context-package:internal-note-review
|
||||
labels:
|
||||
- generated
|
||||
trust_zone: internal
|
||||
attributes:
|
||||
workflow_state: completed
|
||||
- id: export:internal-note-review-bundle
|
||||
type: export
|
||||
parent: workflow-artifact:internal-note-review-run
|
||||
labels:
|
||||
- export
|
||||
trust_zone: external
|
||||
actions:
|
||||
- read
|
||||
- query
|
||||
- search
|
||||
- package
|
||||
- activate_context
|
||||
- export
|
||||
- workflow_run
|
||||
- admin
|
||||
metadata:
|
||||
source: examples/markitect/namespace_resource_manifest.yaml
|
||||
flex_auth_contract: resource-registration-v0
|
||||
155
examples/markitect/protected_system_manifest.yaml
Normal file
155
examples/markitect/protected_system_manifest.yaml
Normal file
@@ -0,0 +1,155 @@
|
||||
id: markitect-tool
|
||||
name: Markitect Tool
|
||||
description: Markitect protected-system namespace for flex-auth.
|
||||
caring_profiles:
|
||||
- caring-0.4.0-rc2
|
||||
actions:
|
||||
- name: read
|
||||
capabilities:
|
||||
- View
|
||||
planes:
|
||||
- Data
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Masked
|
||||
- Plaintext
|
||||
- name: query
|
||||
capabilities:
|
||||
- ViewCollection
|
||||
- Observe
|
||||
planes:
|
||||
- Data
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Aggregated
|
||||
- Masked
|
||||
- name: search
|
||||
capabilities:
|
||||
- ViewCollection
|
||||
- Observe
|
||||
planes:
|
||||
- Data
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Aggregated
|
||||
- Masked
|
||||
- name: package
|
||||
capabilities:
|
||||
- Create
|
||||
- Bind
|
||||
- ViewCollection
|
||||
planes:
|
||||
- Intent
|
||||
- Data
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Masked
|
||||
- name: activate_context
|
||||
capabilities:
|
||||
- Use
|
||||
- Execute
|
||||
planes:
|
||||
- Intent
|
||||
- Policy
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Masked
|
||||
- name: export
|
||||
capabilities:
|
||||
- Export
|
||||
planes:
|
||||
- Data
|
||||
- Audit
|
||||
exposure_modes:
|
||||
- Exportable
|
||||
- Plaintext
|
||||
- name: workflow_run
|
||||
capabilities:
|
||||
- Execute
|
||||
- Operate
|
||||
planes:
|
||||
- Execution
|
||||
- Data
|
||||
- Audit
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Masked
|
||||
- Plaintext
|
||||
- name: admin
|
||||
capabilities:
|
||||
- Configure
|
||||
- Grant
|
||||
- Revoke
|
||||
- Audit
|
||||
planes:
|
||||
- Configuration
|
||||
- Identity
|
||||
- Policy
|
||||
- Audit
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
- Plaintext
|
||||
resource_types:
|
||||
- name: knowledge_base
|
||||
scope_level: Workspace
|
||||
planes:
|
||||
- Intent
|
||||
- Data
|
||||
- name: repository
|
||||
parent_types:
|
||||
- knowledge_base
|
||||
scope_level: Project
|
||||
planes:
|
||||
- Build
|
||||
- Data
|
||||
- name: document
|
||||
parent_types:
|
||||
- repository
|
||||
- knowledge_base
|
||||
scope_level: Resource
|
||||
planes:
|
||||
- Data
|
||||
- name: section
|
||||
parent_types:
|
||||
- document
|
||||
scope_level: Subresource
|
||||
planes:
|
||||
- Data
|
||||
- name: span
|
||||
parent_types:
|
||||
- section
|
||||
- document
|
||||
scope_level: Field
|
||||
planes:
|
||||
- Data
|
||||
- name: context_package
|
||||
parent_types:
|
||||
- knowledge_base
|
||||
- repository
|
||||
- document
|
||||
scope_level: Dataset
|
||||
planes:
|
||||
- Intent
|
||||
- Data
|
||||
- Policy
|
||||
- name: workflow_artifact
|
||||
parent_types:
|
||||
- context_package
|
||||
- document
|
||||
scope_level: Process
|
||||
planes:
|
||||
- Execution
|
||||
- Data
|
||||
- Audit
|
||||
- name: export
|
||||
parent_types:
|
||||
- workflow_artifact
|
||||
- context_package
|
||||
- document
|
||||
scope_level: Record
|
||||
planes:
|
||||
- Data
|
||||
- Audit
|
||||
metadata:
|
||||
source: examples/markitect/protected_system_manifest.yaml
|
||||
namespace_doc: docs/markitect-resource-namespace.md
|
||||
40
examples/markitect/resource_manifest.yaml
Normal file
40
examples/markitect/resource_manifest.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
# Pinned example of the FlexAuthResourceManifest shape.
|
||||
#
|
||||
# Source: markitect-tool/examples/policy/flex-auth-resource-manifest.yaml
|
||||
# (emitted by markitect_tool.policy.enterprise.FlexAuthResourceManifest in
|
||||
# MKTT-WP-0014). Schema: ../../schemas/resource_manifest.schema.json.
|
||||
|
||||
id: markitect-example-knowledge-base
|
||||
system: markitect-tool
|
||||
actions:
|
||||
- read
|
||||
- query
|
||||
- search
|
||||
- package
|
||||
- export
|
||||
resources:
|
||||
- id: knowledge-base:markitect-example
|
||||
type: knowledge_base
|
||||
labels:
|
||||
- public
|
||||
trust_zone: public
|
||||
owner: team:platform-architecture
|
||||
- id: document:public-note
|
||||
type: document
|
||||
parent: knowledge-base:markitect-example
|
||||
path: examples/policy/public-note.md
|
||||
labels:
|
||||
- public
|
||||
trust_zone: public
|
||||
owner: team:platform-architecture
|
||||
- id: document:internal-note
|
||||
type: document
|
||||
parent: knowledge-base:markitect-example
|
||||
path: examples/policy/private/internal-note.md
|
||||
labels:
|
||||
- internal
|
||||
trust_zone: internal
|
||||
owner: team:platform-architecture
|
||||
metadata:
|
||||
source: markitect example policy fixtures
|
||||
flex_auth_contract: resource-registration-v0
|
||||
49
examples/ops-warden/README.md
Normal file
49
examples/ops-warden/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Ops-Warden SSH Signing Policy Gate
|
||||
|
||||
This example is the flex-auth side of ops-warden's opt-in pre-sign gate.
|
||||
When `policy.enabled: true`, ops-warden calls `POST /v1/check` before signing
|
||||
or issuing an SSH certificate.
|
||||
|
||||
Files:
|
||||
|
||||
- `protected_system_manifest.yaml` declares the `ops-warden` protected system,
|
||||
`ssh-certificate` resource type, and `sign` action.
|
||||
- `resource_manifest.yaml` declares fixture SSH certificate actor resources and
|
||||
non-secret policy attributes such as allowed principals and TTL maxima.
|
||||
- `subject_manifest.yaml` declares non-secret fixture actors for `adm`, `agt`,
|
||||
and `atm` signing paths.
|
||||
- `registry_snapshot.json` is the combined local registry used by the CLI and
|
||||
service examples.
|
||||
- `policy_package.md` is the Rego-in-Markdown policy package.
|
||||
- `policy_fixtures.yaml` contains allow and deny expectations for package
|
||||
validation.
|
||||
- `check_request_*.json` files are ops-warden-shaped `/v1/check` requests.
|
||||
|
||||
Run locally:
|
||||
|
||||
```bash
|
||||
flex-auth validate --kind protected-system --file examples/ops-warden/protected_system_manifest.yaml
|
||||
flex-auth validate --kind resource-manifest --file examples/ops-warden/resource_manifest.yaml
|
||||
flex-auth validate --kind subject-manifest --file examples/ops-warden/subject_manifest.yaml
|
||||
flex-auth load-registry --file examples/ops-warden/registry_snapshot.json
|
||||
flex-auth test-policy --file examples/ops-warden/policy_package.md
|
||||
flex-auth check --registry examples/ops-warden/registry_snapshot.json --policy examples/ops-warden/policy_package.md --request examples/ops-warden/check_request_allow_adm.json
|
||||
```
|
||||
|
||||
The fixture public-key fingerprints are examples only. Do not put real keys,
|
||||
OpenBao tokens, or private signing material in these files.
|
||||
|
||||
|
||||
## Production Registry Fixture
|
||||
|
||||
production_registry_snapshot.json is a non-secret fixture generated by
|
||||
ops-warden for FLEX-WP-0007 coverage. It mirrors the current production actor
|
||||
names used by ops-warden inventory and should be refreshed when that inventory
|
||||
changes.
|
||||
|
||||
Validate both registries locally:
|
||||
|
||||
flex-auth load-registry --file examples/ops-warden/registry_snapshot.json
|
||||
flex-auth load-registry --file examples/ops-warden/production_registry_snapshot.json
|
||||
|
||||
The production sync contract is documented in docs/ops-warden-registry-sync.md.
|
||||
23
examples/ops-warden/check_request_allow_adm.json
Normal file
23
examples/ops-warden/check_request_allow_adm.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "platform-steward",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform",
|
||||
"root"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 4,
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
|
||||
}
|
||||
}
|
||||
22
examples/ops-warden/check_request_allow_agt.json
Normal file
22
examples/ops-warden/check_request_allow_agt.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "check:ops-warden-ci-deploy-agent-agt",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "ci-deploy-agent",
|
||||
"type": "agt"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"deploy"
|
||||
],
|
||||
"actor_type": "agt",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
|
||||
}
|
||||
}
|
||||
22
examples/ops-warden/check_request_allow_atm.json
Normal file
22
examples/ops-warden/check_request_allow_atm.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "check:ops-warden-backup-automation-atm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "backup-automation",
|
||||
"type": "atm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/backup-automation",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"backup"
|
||||
],
|
||||
"actor_type": "atm",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-atm-fingerprint"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "check:ops-warden-ci-deploy-agent-agt",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "ci-deploy-agent",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"deploy"
|
||||
],
|
||||
"actor_type": "agt",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "check:ops-warden-ci-deploy-agent-agt",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "ci-deploy-agent",
|
||||
"type": "agt"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"root"
|
||||
],
|
||||
"actor_type": "agt",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "platform-steward",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 4
|
||||
}
|
||||
}
|
||||
22
examples/ops-warden/check_request_deny_ttl_above_max.json
Normal file
22
examples/ops-warden/check_request_deny_ttl_above_max.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "platform-steward",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 12,
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
|
||||
}
|
||||
}
|
||||
22
examples/ops-warden/check_request_deny_unknown_subject.json
Normal file
22
examples/ops-warden/check_request_deny_unknown_subject.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "unknown-actor",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden"
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 4,
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
|
||||
}
|
||||
}
|
||||
337
examples/ops-warden/policy_fixtures.yaml
Normal file
337
examples/ops-warden/policy_fixtures.yaml
Normal file
@@ -0,0 +1,337 @@
|
||||
[
|
||||
{
|
||||
"id": "fixture:ops-warden-adm-sign-allow",
|
||||
"request": {
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "platform-steward",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "platform-steward",
|
||||
"actor_type": "adm",
|
||||
"allowed_subjects": [
|
||||
"platform-steward",
|
||||
"iam:platform-steward"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"platform",
|
||||
"root"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform",
|
||||
"root"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 4,
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "allow",
|
||||
"reason": "signing_policy_matched"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixture:ops-warden-agt-sign-allow",
|
||||
"request": {
|
||||
"id": "check:ops-warden-ci-deploy-agent-agt",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "ci-deploy-agent",
|
||||
"type": "agt"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "ci-deploy-agent",
|
||||
"actor_type": "agt",
|
||||
"allowed_subjects": [
|
||||
"ci-deploy-agent",
|
||||
"iam:ci-deploy-agent"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"deploy",
|
||||
"git"
|
||||
],
|
||||
"max_ttl_hours": 2
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"deploy"
|
||||
],
|
||||
"actor_type": "agt",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "allow",
|
||||
"reason": "signing_policy_matched"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixture:ops-warden-atm-sign-allow",
|
||||
"request": {
|
||||
"id": "check:ops-warden-backup-automation-atm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "backup-automation",
|
||||
"type": "atm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/backup-automation",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "backup-automation",
|
||||
"actor_type": "atm",
|
||||
"allowed_subjects": [
|
||||
"backup-automation",
|
||||
"iam:backup-automation"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"backup"
|
||||
],
|
||||
"max_ttl_hours": 1
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"backup"
|
||||
],
|
||||
"actor_type": "atm",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-atm-fingerprint"
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "allow",
|
||||
"reason": "signing_policy_matched"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixture:ops-warden-unknown-subject-deny",
|
||||
"request": {
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "unknown-actor",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "platform-steward",
|
||||
"actor_type": "adm",
|
||||
"allowed_subjects": [
|
||||
"platform-steward",
|
||||
"iam:platform-steward"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"platform",
|
||||
"root"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 4,
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "deny",
|
||||
"reason": "unknown_subject"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixture:ops-warden-actor-type-mismatch-deny",
|
||||
"request": {
|
||||
"id": "check:ops-warden-ci-deploy-agent-agt",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "ci-deploy-agent",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "ci-deploy-agent",
|
||||
"actor_type": "agt",
|
||||
"allowed_subjects": [
|
||||
"ci-deploy-agent",
|
||||
"iam:ci-deploy-agent"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"deploy",
|
||||
"git"
|
||||
],
|
||||
"max_ttl_hours": 2
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"deploy"
|
||||
],
|
||||
"actor_type": "agt",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "deny",
|
||||
"reason": "actor_type_mismatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixture:ops-warden-ttl-above-max-deny",
|
||||
"request": {
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "platform-steward",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "platform-steward",
|
||||
"actor_type": "adm",
|
||||
"allowed_subjects": [
|
||||
"platform-steward",
|
||||
"iam:platform-steward"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"platform",
|
||||
"root"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 12,
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "deny",
|
||||
"reason": "ttl_out_of_bounds"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixture:ops-warden-disallowed-principal-deny",
|
||||
"request": {
|
||||
"id": "check:ops-warden-ci-deploy-agent-agt",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "ci-deploy-agent",
|
||||
"type": "agt"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "ci-deploy-agent",
|
||||
"actor_type": "agt",
|
||||
"allowed_subjects": [
|
||||
"ci-deploy-agent",
|
||||
"iam:ci-deploy-agent"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"deploy",
|
||||
"git"
|
||||
],
|
||||
"max_ttl_hours": 2
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"root"
|
||||
],
|
||||
"actor_type": "agt",
|
||||
"ttl_hours": 1,
|
||||
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "deny",
|
||||
"reason": "disallowed_principal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixture:ops-warden-missing-fingerprint-deny",
|
||||
"request": {
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "platform-steward",
|
||||
"type": "adm"
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "platform-steward",
|
||||
"actor_type": "adm",
|
||||
"allowed_subjects": [
|
||||
"platform-steward",
|
||||
"iam:platform-steward"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"platform",
|
||||
"root"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"principals": [
|
||||
"platform"
|
||||
],
|
||||
"actor_type": "adm",
|
||||
"ttl_hours": 4
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"effect": "deny",
|
||||
"reason": "missing_pubkey_fingerprint"
|
||||
}
|
||||
}
|
||||
]
|
||||
257
examples/ops-warden/policy_package.md
Normal file
257
examples/ops-warden/policy_package.md
Normal file
@@ -0,0 +1,257 @@
|
||||
---
|
||||
id: ops-warden.ssh-certificate.sign
|
||||
name: Ops-Warden SSH certificate signing
|
||||
namespace: ops-warden:ssh-certificate
|
||||
version: v1
|
||||
status: ready
|
||||
package: flexauth.ops_warden.ssh_signing
|
||||
actions:
|
||||
- sign
|
||||
owner: team:platform-security
|
||||
fixtures:
|
||||
- policy_fixtures.yaml
|
||||
caring:
|
||||
profile: caring-0.4.0-rc2
|
||||
enforce: false
|
||||
canonical_roles:
|
||||
- Operator
|
||||
organization_relations:
|
||||
- ServiceProvider
|
||||
scopes:
|
||||
- level: Platform
|
||||
id: platform:ssh-signing
|
||||
tenant: tenant:platform
|
||||
planes:
|
||||
- Identity
|
||||
- Secret
|
||||
- Audit
|
||||
capabilities:
|
||||
- Use
|
||||
- Operate
|
||||
- Audit
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
conditions:
|
||||
- TimeLimited
|
||||
- Logged
|
||||
restrictions:
|
||||
- PrivilegeEscalationBlocked
|
||||
- SecretAccessBlocked
|
||||
activation:
|
||||
mode: local
|
||||
metadata:
|
||||
source: examples/ops-warden/policy_package.md
|
||||
ops_warden_policy_gate: v2
|
||||
---
|
||||
|
||||
# Ops-Warden SSH Certificate Signing
|
||||
|
||||
This package authorizes ops-warden's opt-in pre-sign policy gate. The caller
|
||||
keeps SSH CA custody, actor inventory, and OpenBao signing; flex-auth decides
|
||||
whether a specific `sign` request is allowed now.
|
||||
|
||||
## Rules
|
||||
|
||||
```rego
|
||||
import future.keywords.contains
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
actor_types := {"adm", "agt", "atm"}
|
||||
|
||||
decision := {"effect": "allow", "reason": "signing_policy_matched"} if {
|
||||
allowed
|
||||
} else := {"effect": "deny", "reason": first_denial} if {
|
||||
true
|
||||
}
|
||||
|
||||
allowed if {
|
||||
input.action == "sign"
|
||||
input.resource.system == "ops-warden"
|
||||
input.resource.type == "ssh-certificate"
|
||||
effective_tenant == "tenant:platform"
|
||||
valid_actor_type
|
||||
subject_type_matches_context
|
||||
actor_type_matches_resource
|
||||
resource_id_matches_actor
|
||||
subject_id_allowed
|
||||
valid_ttl
|
||||
has_pubkey_fingerprint
|
||||
principals_allowed
|
||||
}
|
||||
|
||||
default effective_tenant := ""
|
||||
|
||||
effective_tenant := input.tenant if {
|
||||
is_string(input.tenant)
|
||||
input.tenant != ""
|
||||
} else := input.resource.tenant if {
|
||||
is_string(input.resource.tenant)
|
||||
input.resource.tenant != ""
|
||||
} else := input.subject.tenant if {
|
||||
is_string(input.subject.tenant)
|
||||
input.subject.tenant != ""
|
||||
}
|
||||
|
||||
default first_denial := "no_matching_rule"
|
||||
|
||||
first_denial := "wrong_action" if {
|
||||
input.action != "sign"
|
||||
} else := "wrong_system" if {
|
||||
input.resource.system != "ops-warden"
|
||||
} else := "wrong_resource_type" if {
|
||||
input.resource.type != "ssh-certificate"
|
||||
} else := "wrong_tenant" if {
|
||||
effective_tenant != "tenant:platform"
|
||||
} else := "unknown_actor_resource" if {
|
||||
not has_actor_resource
|
||||
} else := "unknown_subject" if {
|
||||
not subject_id_allowed
|
||||
} else := "actor_type_mismatch" if {
|
||||
not valid_actor_type
|
||||
} else := "actor_type_mismatch" if {
|
||||
not subject_type_matches_context
|
||||
} else := "actor_type_mismatch" if {
|
||||
not actor_type_matches_resource
|
||||
} else := "actor_resource_mismatch" if {
|
||||
not resource_id_matches_actor
|
||||
} else := "ttl_out_of_bounds" if {
|
||||
not valid_ttl
|
||||
} else := "missing_pubkey_fingerprint" if {
|
||||
not has_pubkey_fingerprint
|
||||
} else := "missing_principal" if {
|
||||
not has_principals
|
||||
} else := "disallowed_principal" if {
|
||||
count(disallowed_principals) > 0
|
||||
}
|
||||
|
||||
has_actor_resource if {
|
||||
is_string(input.resource.attributes.actor_id)
|
||||
input.resource.attributes.actor_id != ""
|
||||
}
|
||||
|
||||
valid_actor_type if {
|
||||
is_string(input.context.actor_type)
|
||||
input.context.actor_type in actor_types
|
||||
}
|
||||
|
||||
subject_type_matches_context if {
|
||||
input.subject.type == input.context.actor_type
|
||||
}
|
||||
|
||||
subject_type_matches_context if {
|
||||
input.subject.attributes.actor_type == input.context.actor_type
|
||||
}
|
||||
|
||||
actor_type_matches_resource if {
|
||||
input.context.actor_type == input.resource.attributes.actor_type
|
||||
}
|
||||
|
||||
resource_id_matches_actor if {
|
||||
input.resource.id == sprintf("ssh-cert:actor/%s", [input.resource.attributes.actor_id])
|
||||
}
|
||||
|
||||
subject_id_allowed if {
|
||||
input.subject.id in input.resource.attributes.allowed_subjects
|
||||
}
|
||||
|
||||
has_ttl if {
|
||||
is_number(input.context.ttl_hours)
|
||||
}
|
||||
|
||||
valid_ttl if {
|
||||
has_ttl
|
||||
input.context.ttl_hours > 0
|
||||
input.context.ttl_hours <= input.resource.attributes.max_ttl_hours
|
||||
}
|
||||
|
||||
has_pubkey_fingerprint if {
|
||||
is_string(input.context.pubkey_fingerprint)
|
||||
input.context.pubkey_fingerprint != ""
|
||||
}
|
||||
|
||||
has_principals if {
|
||||
count(input.context.principals) > 0
|
||||
}
|
||||
|
||||
principals_allowed if {
|
||||
has_principals
|
||||
count(disallowed_principals) == 0
|
||||
}
|
||||
|
||||
allowed_principal(principal) if {
|
||||
principal in input.resource.attributes.allowed_principals
|
||||
}
|
||||
|
||||
disallowed_principals contains principal if {
|
||||
principal := input.context.principals[_]
|
||||
not allowed_principal(principal)
|
||||
}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```rego test
|
||||
package flexauth.ops_warden.ssh_signing_test
|
||||
|
||||
import future.keywords.if
|
||||
import data.flexauth.ops_warden.ssh_signing
|
||||
|
||||
adm_request := {
|
||||
"id": "check:ops-warden-platform-steward-adm",
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {"id": "platform-steward", "type": "adm"},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"system": "ops-warden",
|
||||
"attributes": {
|
||||
"actor_id": "platform-steward",
|
||||
"actor_type": "adm",
|
||||
"allowed_subjects": ["platform-steward", "iam:platform-steward"],
|
||||
"allowed_principals": ["platform", "root"],
|
||||
"max_ttl_hours": 8
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"actor_type": "adm",
|
||||
"principals": ["platform"],
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint",
|
||||
"ttl_hours": 4
|
||||
}
|
||||
}
|
||||
|
||||
test_adm_sign_allowed if {
|
||||
ssh_signing.decision.effect == "allow" with input as adm_request
|
||||
}
|
||||
|
||||
test_high_ttl_denied if {
|
||||
ssh_signing.decision.reason == "ttl_out_of_bounds" with input as {
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {"id": "platform-steward", "type": "adm"},
|
||||
"action": "sign",
|
||||
"resource": adm_request.resource,
|
||||
"context": {
|
||||
"actor_type": "adm",
|
||||
"principals": ["platform"],
|
||||
"pubkey_fingerprint": "SHA256:example-adm-fingerprint",
|
||||
"ttl_hours": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_missing_fingerprint_denied if {
|
||||
ssh_signing.decision.reason == "missing_pubkey_fingerprint" with input as {
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {"id": "platform-steward", "type": "adm"},
|
||||
"action": "sign",
|
||||
"resource": adm_request.resource,
|
||||
"context": {
|
||||
"actor_type": "adm",
|
||||
"principals": ["platform"],
|
||||
"ttl_hours": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
450
examples/ops-warden/production_registry_snapshot.json
Normal file
450
examples/ops-warden/production_registry_snapshot.json
Normal file
@@ -0,0 +1,450 @@
|
||||
{
|
||||
"systems": [
|
||||
{
|
||||
"id": "ops-warden",
|
||||
"name": "Ops Warden",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": "ssh-certificate",
|
||||
"scope_level": "Resource",
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"metadata": {
|
||||
"description": "Short-lived SSH certificate signing request."
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "sign",
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"metadata": {
|
||||
"required_context": [
|
||||
"principals",
|
||||
"actor_type",
|
||||
"pubkey_fingerprint",
|
||||
"ttl_hours"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"caring_profiles": [
|
||||
"caring-0.4.0-rc2"
|
||||
],
|
||||
"metadata": {
|
||||
"flex_auth_contract": "protected-system-v0",
|
||||
"ops_warden_policy_gate": "v2",
|
||||
"policy_enabled_config": "policy.enabled",
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resource_manifests": [
|
||||
{
|
||||
"id": "ops-warden-ssh-certificates",
|
||||
"system": "ops-warden",
|
||||
"resources": [
|
||||
{
|
||||
"id": "ssh-cert:actor/adm-example",
|
||||
"type": "ssh-certificate",
|
||||
"labels": [
|
||||
"ssh-signing",
|
||||
"adm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "adm-example",
|
||||
"actor_type": "adm",
|
||||
"allowed_subjects": [
|
||||
"adm-example",
|
||||
"iam:adm-example"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"adm-full"
|
||||
],
|
||||
"max_ttl_hours": 48
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ssh-cert:actor/agt-codex-interhub-bootstrap",
|
||||
"type": "ssh-certificate",
|
||||
"labels": [
|
||||
"ssh-signing",
|
||||
"agt"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "agt-codex-interhub-bootstrap",
|
||||
"actor_type": "agt",
|
||||
"allowed_subjects": [
|
||||
"agt-codex-interhub-bootstrap",
|
||||
"iam:agt-codex-interhub-bootstrap"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"agt-interhub-bootstrap"
|
||||
],
|
||||
"max_ttl_hours": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ssh-cert:actor/agt-state-hub-bridge",
|
||||
"type": "ssh-certificate",
|
||||
"labels": [
|
||||
"ssh-signing",
|
||||
"agt"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "agt-state-hub-bridge",
|
||||
"actor_type": "agt",
|
||||
"allowed_subjects": [
|
||||
"agt-state-hub-bridge",
|
||||
"iam:agt-state-hub-bridge"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"agt-task-bridge"
|
||||
],
|
||||
"max_ttl_hours": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ssh-cert:actor/atm-backup-daily",
|
||||
"type": "ssh-certificate",
|
||||
"labels": [
|
||||
"ssh-signing",
|
||||
"atm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "atm-backup-daily",
|
||||
"actor_type": "atm",
|
||||
"allowed_subjects": [
|
||||
"atm-backup-daily",
|
||||
"iam:atm-backup-daily"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"atm-backup-daily"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"sign"
|
||||
],
|
||||
"caring_profile": "caring-0.4.0-rc2",
|
||||
"metadata": {
|
||||
"flex_auth_contract": "resource-registration-v0",
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tenants": [
|
||||
{
|
||||
"id": "tenant:platform",
|
||||
"name": "Platform Tenant"
|
||||
}
|
||||
],
|
||||
"subjects": [
|
||||
{
|
||||
"id": "adm-example",
|
||||
"type": "Agent",
|
||||
"display_name": "Example human operator \u2014 replace with per-person adm-* actors",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:ops-warden-admins"
|
||||
],
|
||||
"tenant": "tenant:platform",
|
||||
"metadata": {
|
||||
"actor_type": "adm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agt-codex-interhub-bootstrap",
|
||||
"type": "Agent",
|
||||
"display_name": "Short-lived agent access for attended Inter-Hub bootstrap",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:ops-warden-agents"
|
||||
],
|
||||
"tenant": "tenant:platform",
|
||||
"metadata": {
|
||||
"actor_type": "agt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "agt-state-hub-bridge",
|
||||
"type": "Agent",
|
||||
"display_name": "ops-bridge tunnel agent for state-hub",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:ops-warden-agents"
|
||||
],
|
||||
"tenant": "tenant:platform",
|
||||
"metadata": {
|
||||
"actor_type": "agt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "atm-backup-daily",
|
||||
"type": "Automation",
|
||||
"display_name": "Example nightly automation actor",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:ops-warden-automations"
|
||||
],
|
||||
"tenant": "tenant:platform",
|
||||
"metadata": {
|
||||
"actor_type": "atm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "group:ops-warden-admins",
|
||||
"display_name": "Ops Warden Admins",
|
||||
"members": [
|
||||
"adm-example"
|
||||
],
|
||||
"tenant": "tenant:platform"
|
||||
},
|
||||
{
|
||||
"id": "group:ops-warden-agents",
|
||||
"display_name": "Ops Warden Agents",
|
||||
"members": [
|
||||
"agt-codex-interhub-bootstrap",
|
||||
"agt-state-hub-bridge"
|
||||
],
|
||||
"tenant": "tenant:platform"
|
||||
},
|
||||
{
|
||||
"id": "group:ops-warden-automations",
|
||||
"display_name": "Ops Warden Automations",
|
||||
"members": [
|
||||
"atm-backup-daily"
|
||||
],
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"id": "rel:adm-example-sign-adm-example",
|
||||
"system": "ops-warden",
|
||||
"subject": "group:ops-warden-admins",
|
||||
"relation": "signer",
|
||||
"object": "ssh-cert:actor/adm-example",
|
||||
"tenant": "tenant:platform",
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"caring": {
|
||||
"id": "descriptor:ops-warden-adm-signer",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"canonical_role": "Operator",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "ssh-cert:actor/adm-example",
|
||||
"tenant": "tenant:platform",
|
||||
"resource": "ssh-cert:actor/adm-example"
|
||||
},
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"restrictions": [
|
||||
"PrivilegeEscalationBlocked",
|
||||
"SecretAccessBlocked"
|
||||
],
|
||||
"access_path": "mediated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel:agt-codex-interhub-bootstrap-sign-agt-codex-interhub-bootstrap",
|
||||
"system": "ops-warden",
|
||||
"subject": "group:ops-warden-agents",
|
||||
"relation": "signer",
|
||||
"object": "ssh-cert:actor/agt-codex-interhub-bootstrap",
|
||||
"tenant": "tenant:platform",
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"caring": {
|
||||
"id": "descriptor:ops-warden-agt-signer",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"canonical_role": "Operator",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "ssh-cert:actor/agt-codex-interhub-bootstrap",
|
||||
"tenant": "tenant:platform",
|
||||
"resource": "ssh-cert:actor/agt-codex-interhub-bootstrap"
|
||||
},
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"restrictions": [
|
||||
"PrivilegeEscalationBlocked",
|
||||
"SecretAccessBlocked"
|
||||
],
|
||||
"access_path": "mediated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel:agt-state-hub-bridge-sign-agt-state-hub-bridge",
|
||||
"system": "ops-warden",
|
||||
"subject": "group:ops-warden-agents",
|
||||
"relation": "signer",
|
||||
"object": "ssh-cert:actor/agt-state-hub-bridge",
|
||||
"tenant": "tenant:platform",
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"caring": {
|
||||
"id": "descriptor:ops-warden-agt-signer",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"canonical_role": "Operator",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "ssh-cert:actor/agt-state-hub-bridge",
|
||||
"tenant": "tenant:platform",
|
||||
"resource": "ssh-cert:actor/agt-state-hub-bridge"
|
||||
},
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"restrictions": [
|
||||
"PrivilegeEscalationBlocked",
|
||||
"SecretAccessBlocked"
|
||||
],
|
||||
"access_path": "mediated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel:atm-backup-daily-sign-atm-backup-daily",
|
||||
"system": "ops-warden",
|
||||
"subject": "group:ops-warden-automations",
|
||||
"relation": "signer",
|
||||
"object": "ssh-cert:actor/atm-backup-daily",
|
||||
"tenant": "tenant:platform",
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"caring": {
|
||||
"id": "descriptor:ops-warden-atm-signer",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"canonical_role": "Operator",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "ssh-cert:actor/atm-backup-daily",
|
||||
"tenant": "tenant:platform",
|
||||
"resource": "ssh-cert:actor/atm-backup-daily"
|
||||
},
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"restrictions": [
|
||||
"PrivilegeEscalationBlocked",
|
||||
"SecretAccessBlocked"
|
||||
],
|
||||
"access_path": "mediated"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
36
examples/ops-warden/protected_system_manifest.yaml
Normal file
36
examples/ops-warden/protected_system_manifest.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
id: ops-warden
|
||||
name: Ops Warden
|
||||
resource_types:
|
||||
- name: ssh-certificate
|
||||
scope_level: Resource
|
||||
planes:
|
||||
- Identity
|
||||
- Secret
|
||||
- Audit
|
||||
metadata:
|
||||
description: Short-lived SSH certificate signing request.
|
||||
actions:
|
||||
- name: sign
|
||||
capabilities:
|
||||
- Use
|
||||
- Operate
|
||||
- Audit
|
||||
planes:
|
||||
- Identity
|
||||
- Secret
|
||||
- Audit
|
||||
exposure_modes:
|
||||
- Metadata
|
||||
metadata:
|
||||
required_context:
|
||||
- principals
|
||||
- actor_type
|
||||
- pubkey_fingerprint
|
||||
- ttl_hours
|
||||
caring_profiles:
|
||||
- caring-0.4.0-rc2
|
||||
metadata:
|
||||
flex_auth_contract: protected-system-v0
|
||||
ops_warden_policy_gate: v2
|
||||
policy_enabled_config: policy.enabled
|
||||
tenant: tenant:platform
|
||||
366
examples/ops-warden/registry_snapshot.json
Normal file
366
examples/ops-warden/registry_snapshot.json
Normal file
@@ -0,0 +1,366 @@
|
||||
{
|
||||
"systems": [
|
||||
{
|
||||
"id": "ops-warden",
|
||||
"name": "Ops Warden",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": "ssh-certificate",
|
||||
"scope_level": "Resource",
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"metadata": {
|
||||
"description": "Short-lived SSH certificate signing request."
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "sign",
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"metadata": {
|
||||
"required_context": [
|
||||
"principals",
|
||||
"actor_type",
|
||||
"pubkey_fingerprint",
|
||||
"ttl_hours"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"caring_profiles": [
|
||||
"caring-0.4.0-rc2"
|
||||
],
|
||||
"metadata": {
|
||||
"flex_auth_contract": "protected-system-v0",
|
||||
"ops_warden_policy_gate": "v2",
|
||||
"policy_enabled_config": "policy.enabled",
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resource_manifests": [
|
||||
{
|
||||
"id": "ops-warden-ssh-certificates",
|
||||
"system": "ops-warden",
|
||||
"resources": [
|
||||
{
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"type": "ssh-certificate",
|
||||
"labels": [
|
||||
"ssh-signing",
|
||||
"adm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "platform-steward",
|
||||
"actor_type": "adm",
|
||||
"allowed_subjects": [
|
||||
"platform-steward",
|
||||
"iam:platform-steward"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"platform",
|
||||
"root"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"type": "ssh-certificate",
|
||||
"labels": [
|
||||
"ssh-signing",
|
||||
"agt"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "ci-deploy-agent",
|
||||
"actor_type": "agt",
|
||||
"allowed_subjects": [
|
||||
"ci-deploy-agent",
|
||||
"iam:ci-deploy-agent"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"deploy",
|
||||
"git"
|
||||
],
|
||||
"max_ttl_hours": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ssh-cert:actor/backup-automation",
|
||||
"type": "ssh-certificate",
|
||||
"labels": [
|
||||
"ssh-signing",
|
||||
"atm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "backup-automation",
|
||||
"actor_type": "atm",
|
||||
"allowed_subjects": [
|
||||
"backup-automation",
|
||||
"iam:backup-automation"
|
||||
],
|
||||
"allowed_principals": [
|
||||
"backup"
|
||||
],
|
||||
"max_ttl_hours": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"sign"
|
||||
],
|
||||
"caring_profile": "caring-0.4.0-rc2",
|
||||
"metadata": {
|
||||
"flex_auth_contract": "resource-registration-v0",
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tenants": [
|
||||
{
|
||||
"id": "tenant:platform",
|
||||
"name": "Platform Tenant"
|
||||
}
|
||||
],
|
||||
"subjects": [
|
||||
{
|
||||
"id": "platform-steward",
|
||||
"type": "Agent",
|
||||
"display_name": "Platform Steward",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:ops-warden-admins"
|
||||
],
|
||||
"tenant": "tenant:platform",
|
||||
"metadata": {
|
||||
"actor_type": "adm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ci-deploy-agent",
|
||||
"type": "Agent",
|
||||
"display_name": "CI Deploy Agent",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:ops-warden-agents"
|
||||
],
|
||||
"tenant": "tenant:platform",
|
||||
"metadata": {
|
||||
"actor_type": "agt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "backup-automation",
|
||||
"type": "Automation",
|
||||
"display_name": "Backup Automation",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:ops-warden-automations"
|
||||
],
|
||||
"tenant": "tenant:platform",
|
||||
"metadata": {
|
||||
"actor_type": "atm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "group:ops-warden-admins",
|
||||
"display_name": "Ops Warden Admin Actors",
|
||||
"members": [
|
||||
"platform-steward"
|
||||
],
|
||||
"tenant": "tenant:platform"
|
||||
},
|
||||
{
|
||||
"id": "group:ops-warden-agents",
|
||||
"display_name": "Ops Warden Agent Actors",
|
||||
"members": [
|
||||
"ci-deploy-agent"
|
||||
],
|
||||
"tenant": "tenant:platform"
|
||||
},
|
||||
{
|
||||
"id": "group:ops-warden-automations",
|
||||
"display_name": "Ops Warden Automation Actors",
|
||||
"members": [
|
||||
"backup-automation"
|
||||
],
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"id": "rel:platform-steward-sign-platform-steward",
|
||||
"system": "ops-warden",
|
||||
"subject": "group:ops-warden-admins",
|
||||
"relation": "signer",
|
||||
"object": "ssh-cert:actor/platform-steward",
|
||||
"tenant": "tenant:platform",
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"caring": {
|
||||
"id": "descriptor:ops-warden-adm-signer",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"canonical_role": "Operator",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "ssh-cert:actor/platform-steward",
|
||||
"tenant": "tenant:platform",
|
||||
"resource": "ssh-cert:actor/platform-steward"
|
||||
},
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"restrictions": [
|
||||
"PrivilegeEscalationBlocked",
|
||||
"SecretAccessBlocked"
|
||||
],
|
||||
"access_path": "mediated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel:ci-deploy-agent-sign-ci-deploy-agent",
|
||||
"system": "ops-warden",
|
||||
"subject": "group:ops-warden-agents",
|
||||
"relation": "signer",
|
||||
"object": "ssh-cert:actor/ci-deploy-agent",
|
||||
"tenant": "tenant:platform",
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"caring": {
|
||||
"id": "descriptor:ops-warden-agt-signer",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"canonical_role": "Operator",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "ssh-cert:actor/ci-deploy-agent",
|
||||
"tenant": "tenant:platform",
|
||||
"resource": "ssh-cert:actor/ci-deploy-agent"
|
||||
},
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"restrictions": [
|
||||
"PrivilegeEscalationBlocked",
|
||||
"SecretAccessBlocked"
|
||||
],
|
||||
"access_path": "mediated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rel:backup-automation-sign-backup-automation",
|
||||
"system": "ops-warden",
|
||||
"subject": "group:ops-warden-automations",
|
||||
"relation": "signer",
|
||||
"object": "ssh-cert:actor/backup-automation",
|
||||
"tenant": "tenant:platform",
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"caring": {
|
||||
"id": "descriptor:ops-warden-atm-signer",
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"subject_type": "Group",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"canonical_role": "Operator",
|
||||
"scope": {
|
||||
"level": "Resource",
|
||||
"id": "ssh-cert:actor/backup-automation",
|
||||
"tenant": "tenant:platform",
|
||||
"resource": "ssh-cert:actor/backup-automation"
|
||||
},
|
||||
"planes": [
|
||||
"Identity",
|
||||
"Secret",
|
||||
"Audit"
|
||||
],
|
||||
"capabilities": [
|
||||
"Use",
|
||||
"Operate",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Metadata"
|
||||
],
|
||||
"conditions": [
|
||||
"TimeLimited",
|
||||
"Logged"
|
||||
],
|
||||
"restrictions": [
|
||||
"PrivilegeEscalationBlocked",
|
||||
"SecretAccessBlocked"
|
||||
],
|
||||
"access_path": "mediated"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
59
examples/ops-warden/resource_manifest.yaml
Normal file
59
examples/ops-warden/resource_manifest.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
id: ops-warden-ssh-certificates
|
||||
system: ops-warden
|
||||
resources:
|
||||
- id: ssh-cert:actor/platform-steward
|
||||
type: ssh-certificate
|
||||
labels:
|
||||
- ssh-signing
|
||||
- adm
|
||||
trust_zone: platform
|
||||
owner: team:platform-security
|
||||
attributes:
|
||||
actor_id: platform-steward
|
||||
actor_type: adm
|
||||
allowed_subjects:
|
||||
- platform-steward
|
||||
- iam:platform-steward
|
||||
allowed_principals:
|
||||
- platform
|
||||
- root
|
||||
max_ttl_hours: 8
|
||||
- id: ssh-cert:actor/ci-deploy-agent
|
||||
type: ssh-certificate
|
||||
labels:
|
||||
- ssh-signing
|
||||
- agt
|
||||
trust_zone: platform
|
||||
owner: team:platform-security
|
||||
attributes:
|
||||
actor_id: ci-deploy-agent
|
||||
actor_type: agt
|
||||
allowed_subjects:
|
||||
- ci-deploy-agent
|
||||
- iam:ci-deploy-agent
|
||||
allowed_principals:
|
||||
- deploy
|
||||
- git
|
||||
max_ttl_hours: 2
|
||||
- id: ssh-cert:actor/backup-automation
|
||||
type: ssh-certificate
|
||||
labels:
|
||||
- ssh-signing
|
||||
- atm
|
||||
trust_zone: platform
|
||||
owner: team:platform-security
|
||||
attributes:
|
||||
actor_id: backup-automation
|
||||
actor_type: atm
|
||||
allowed_subjects:
|
||||
- backup-automation
|
||||
- iam:backup-automation
|
||||
allowed_principals:
|
||||
- backup
|
||||
max_ttl_hours: 1
|
||||
actions:
|
||||
- sign
|
||||
caring_profile: caring-0.4.0-rc2
|
||||
metadata:
|
||||
flex_auth_contract: resource-registration-v0
|
||||
tenant: tenant:platform
|
||||
54
examples/ops-warden/subject_manifest.yaml
Normal file
54
examples/ops-warden/subject_manifest.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
id: subjects:ops-warden-platform
|
||||
tenants:
|
||||
- id: tenant:platform
|
||||
name: Platform Tenant
|
||||
subjects:
|
||||
- id: platform-steward
|
||||
type: Agent
|
||||
display_name: Platform Steward
|
||||
organization_relation: ServiceProvider
|
||||
roles:
|
||||
- Operator
|
||||
groups:
|
||||
- group:ops-warden-admins
|
||||
tenant: tenant:platform
|
||||
metadata:
|
||||
actor_type: adm
|
||||
- id: ci-deploy-agent
|
||||
type: Agent
|
||||
display_name: CI Deploy Agent
|
||||
organization_relation: ServiceProvider
|
||||
roles:
|
||||
- Operator
|
||||
groups:
|
||||
- group:ops-warden-agents
|
||||
tenant: tenant:platform
|
||||
metadata:
|
||||
actor_type: agt
|
||||
- id: backup-automation
|
||||
type: Automation
|
||||
display_name: Backup Automation
|
||||
organization_relation: ServiceProvider
|
||||
roles:
|
||||
- Operator
|
||||
groups:
|
||||
- group:ops-warden-automations
|
||||
tenant: tenant:platform
|
||||
metadata:
|
||||
actor_type: atm
|
||||
groups:
|
||||
- id: group:ops-warden-admins
|
||||
display_name: Ops Warden Admin Actors
|
||||
members:
|
||||
- platform-steward
|
||||
tenant: tenant:platform
|
||||
- id: group:ops-warden-agents
|
||||
display_name: Ops Warden Agent Actors
|
||||
members:
|
||||
- ci-deploy-agent
|
||||
tenant: tenant:platform
|
||||
- id: group:ops-warden-automations
|
||||
display_name: Ops Warden Automation Actors
|
||||
members:
|
||||
- backup-automation
|
||||
tenant: tenant:platform
|
||||
100
examples/topaz/README.md
Normal file
100
examples/topaz/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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`, `identity`, `group`, `tenant`, `knowledge_base`, `document`)
|
||||
and relations (`identifier`, `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
|
||||
23
examples/topaz/data/objects.json
Normal file
23
examples/topaz/data/objects.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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": "identity", "id": "identity:alice@example.test", "properties": {"identifier": "alice@example.test", "subject": "alice@example.test"}},
|
||||
{"type": "user", "id": "bob@example.test", "display_name": "Bob (reader)"},
|
||||
{"type": "identity", "id": "identity:bob@example.test", "properties": {"identifier": "bob@example.test", "subject": "bob@example.test"}},
|
||||
{"type": "user", "id": "eve@example.test", "display_name": "Eve (outsider)"},
|
||||
{"type": "identity", "id": "identity:eve@example.test", "properties": {"identifier": "eve@example.test", "subject": "eve@example.test"}},
|
||||
{
|
||||
"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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
examples/topaz/data/relations.json
Normal file
13
examples/topaz/data/relations.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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": "identity", "object_id": "identity:alice@example.test", "relation": "identifier", "subject_type": "user", "subject_id": "alice@example.test"},
|
||||
{"object_type": "identity", "object_id": "identity:bob@example.test", "relation": "identifier", "subject_type": "user", "subject_id": "bob@example.test"},
|
||||
{"object_type": "identity", "object_id": "identity:eve@example.test", "relation": "identifier", "subject_type": "user", "subject_id": "eve@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:
|
||||
53
examples/topaz/manifest.yaml
Normal file
53
examples/topaz/manifest.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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
|
||||
|
||||
identity:
|
||||
relations:
|
||||
identifier: 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"
|
||||
41
go.mod
Normal file
41
go.mod
Normal file
@@ -0,0 +1,41 @@
|
||||
module github.com/netkingdom/flex-auth
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/open-policy-agent/opa v0.70.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/yashtewari/glob-intersection v0.2.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
154
go.sum
Normal file
154
go.sum
Normal file
@@ -0,0 +1,154 @@
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
|
||||
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
|
||||
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
|
||||
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
|
||||
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/open-policy-agent/opa v0.70.0 h1:B3cqCN2iQAyKxK6+GI+N40uqkin+wzIrM7YA60t9x1U=
|
||||
github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
|
||||
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
|
||||
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user