Files
shard-wiki/docs/access-model-blueprint.md
tegwick a00a457f44 Establish clean starting point: workplan, access model, INTENT amendments
Add SHARD-WP-0001 workplan backed by the StateHub yawex-requirements
workstream (6 design tasks, all todo). Includes access-model blueprint,
NetKingdom integration requirements draft, and INTENT amendments for
open-by-default authorization delegated to external identity providers.
2026-06-08 12:23:54 +02:00

146 lines
8.9 KiB
Markdown

# Architecture blueprint — shard-wiki access & history model
Status: **draft for review** · Date: 2026-06-08 · Resolves pending decision
"Scope of a minimal access model in shard-wiki core".
This blueprint settles how shard-wiki handles access control and change history. It
realizes the INTENT principles *Open by default, progressively governed*, *History as
the safety net*, and *Authorization in core, authentication delegated*.
---
## 1. The core idea: one core, a ladder of modes
shard-wiki ships **one** authorization core. What changes between deployments is how much
of it is switched on. No re-architecture is needed to climb the ladder — only
configuration and the presence (or absence) of an external identity provider.
This mirrors the NetKingdom **capability-progression ladder** ("capability-driven, not
scale-driven"): start lightweight, expand when the need appears.
| Mode | Identity provider | Who can read/write | External deps | Analogue |
|------|------------------|--------------------|---------------|----------|
| **L0 — Open (c2)** | none (everyone is `anonymous`) | everyone reads + writes | **none** | Ward Cunningham's original c2 wiki |
| **L1 — Attributed** | local/optional | open writes, but edits are attributed when an identity is presented | none | classic "sign your edits" wiki |
| **L2 — Authenticated** | user-engine (OIDC via net-kingdom lightweight) | authenticated principals; simple read-all / write-authenticated | user-engine + net-kingdom (OIDC/PKCE) | small team wiki |
| **L3 — Role/Group** | user-engine memberships | per-shard/per-namespace roles (reader/author/maintainer) | user-engine groups | departmental wiki |
| **L4 — Multi-tenant enterprise** | user-engine + net-kingdom expanded (Keycloak/SAML) | per-tenant isolation, per-page ACL, SSO/MFA, audit | full IAM stack | enterprise-grade |
**Invariant:** L0 must always be reachable with zero external dependencies. Access control
is *additive*; removing the identity provider degrades gracefully back down the ladder, it
never bricks the wiki.
---
## 2. Why history is the floor, not access control
In L0 the wiki trusts every writer. The protection against accidental loss, vandalism, or
mistakes is therefore **not** gatekeeping but **complete, recoverable history**:
- Every write is a Git commit on the information space's coordination layer (per INTENT's
Git-addressable coordination principle).
- Any revision of any page is restorable; deletion is a commit, never destruction.
- History exists identically in **all** modes — L4's access control *hardens* the wiki but
the recoverability guarantee is the same one that makes L0 safe to run open.
This is the inversion of yawex, which gated with htpasswd and kept only a single `page~`
backup. We keep the openness, replace the gate with history, and make the gate optional.
---
## 3. Component model
```
┌─────────────────────────────────────────────┐
│ shard-wiki │
request ───► │ ┌────────────┐ ┌──────────────────────┐ │
(principal?) │ │ Policy │ │ Authorization core │ │
│ │ Enforcement├──►│ (PDP): capability │ │
│ │ Point (PEP)│ │ decisions per │ │
│ └─────┬───────┘ │ page / shard / tenant│ │
│ │ └──────────┬───────────┘ │
│ ▼ │ │
│ ┌────────────┐ ┌──────▼───────────┐ │
│ │ Shard │ │ IdentityProvider │ │
│ │ adapters │ │ (interface) │ │
│ │ (+capability│ └──────┬───────────┘ │
│ │ profile) │ │ │
│ └─────┬───────┘ │ │
│ ▼ │ │
│ Git-backed coordination journal │ │
└──────────────────────────────────┼─────────────┘
│ (L2+ only)
┌────────────────────────▼───────────────────┐
│ user-engine (accounts, memberships, │
│ profiles, audit, events) │
│ backed by net-kingdom IAM │
│ (OIDC/PKCE lightweight · Keycloak/SAML exp) │
└─────────────────────────────────────────────┘
```
**Owned by shard-wiki core:**
- **Principal** — a resolved actor: `anonymous`, or an identity token + claims from the
provider. Core never stores credentials.
- **IdentityProvider interface** — a thin pluggable contract. The **null provider**
(everyone = `anonymous`) is the L0 default and ships in-core.
- **Authorization core (PDP)** — pure capability decisions: given (principal, action,
target page/shard/tenant), return allow/deny. Actions: `read, write, patch, merge,
administer`. Layered on each adapter's **capability profile** (a shard that can't write
can't be written regardless of policy).
- **Policy Enforcement Point (PEP)** — wraps every adapter operation; calls the PDP.
- **Tenant boundary** — a *root entity* is the unit of multi-tenancy. Shards attach to a
root; an L4 tenant maps to a root entity (or a set of them).
- **Change history** — Git commits on the coordination journal; the recovery substrate.
**Delegated to the identity provider (L2+):**
- Authentication (who you are) — net-kingdom IAM (OIDC/PKCE → Keycloak/SAML).
- Identity lifecycle, user directory, credentials, secrets — net-kingdom / user-engine.
- Memberships, groups, profiles, org structure, audit sink — user-engine.
---
## 4. Request flow (L4, fully governed)
1. Caller presents an OIDC token (issued by net-kingdom IAM) to shard-wiki.
2. The configured **IdentityProvider** validates the token and resolves a **Principal**
(subject + tenant + group/role claims), enriching via **user-engine** memberships.
3. The **PEP** intercepts the requested action on a target page/shard.
4. The **PDP** decides using: tenant isolation → shard role bindings → optional per-page
ACL → adapter capability profile.
5. On allow, the adapter performs the op; the write lands as a **Git commit** and an
**audit event** is emitted to user-engine.
6. On deny, the op is refused; nothing mutates.
**The same code path at L0:** step 1 is skipped, the null provider returns `anonymous`,
the PDP's open policy allows read+write, the write still lands as a Git commit. History is
identical; only the gate differs.
---
## 5. Design rules
- **Fail open only at L0, fail closed at L2+.** The mode is explicit configuration, never
inferred from whether the provider happens to be reachable (a flaky IAM must not silently
open an enterprise wiki). If an L2+ deployment loses its provider, it denies, it does not
fall back to open.
- **Authorization is pure and offline-capable.** Once a Principal is resolved, decisions
need no network call — role/ACL data is carried on the Principal or cached, so a federated
read of a projected page doesn't require a live IAM round-trip per page.
- **Per-page ACL is opt-in (L4).** Default scoping is per-shard / per-namespace (L3). Per-page
ACL is supported but off by default to avoid yawex's per-directory `AccessControl` sprawl.
- **Provenance carries authorization context.** A federated page records not just its source
shard and freshness but the authorization context under which it was read, so the union
view never leaks content a principal couldn't see at source.
---
## 6. Open questions (carry into requirements)
1. Token format/claims contract with net-kingdom (OIDC scopes, tenant claim name).
2. user-engine membership/role query shape and caching/TTL.
3. Audit event schema shard-wiki emits to user-engine.
4. How tenant ↔ root-entity mapping is configured and discovered.
5. Whether L1 "attributed but open" is worth shipping or we jump L0 → L2.
See `requirements-user-engine-netkingdom.md` for the concrete asks these imply.