From 4d9bad265dc432db612962dc6908cb91734a78dc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 00:39:47 +0000 Subject: [PATCH 1/4] ogar-vocab: mint auth_surrealdb (0x0B05), the SurrealDB IAM provider profile is-a auth_store, like the Zitadel / Zanzibar / Ory Keto profiles. Claims: subject ID, roles RL, namespace NS; DB and AC narrow the scope. Paired with the lance-graph contract mirror row and AuthProvider::SurrealDb (merge together). Count pins moved 98 -> 99 (class_ids::ALL) and 4 -> 5 (Auth). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- crates/ogar-class-view/src/lib.rs | 2 ++ crates/ogar-vocab/src/capability_registry.rs | 2 +- crates/ogar-vocab/src/lib.rs | 29 ++++++++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index 4416432f..016ca88a 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -73,6 +73,7 @@ use ogar_vocab::{ anatomical_structure, auth_ory_keto, auth_store, + auth_surrealdb, auth_zanzibar, auth_zitadel, automation_trigger, @@ -267,6 +268,7 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("auth_zitadel", auth_zitadel()), ("auth_zanzibar", auth_zanzibar()), ("auth_ory_keto", auth_ory_keto()), + ("auth_surrealdb", auth_surrealdb()), // ── 0x0DXX — HR cluster (closes the final 4-of-11 odoo-rs #14 gap) ── ("hr_employee", hr_employee()), ("hr_department", hr_department()), diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index c477b5c8..ae1d24af 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -377,7 +377,7 @@ fn resolve_concept_row(id: u16) -> Option<(&'static str, u16)> { mod the_canon_carries_no_palette_rows { #[test] fn no_0x17xx_row_reached_the_globally_mirrored_codebook() { - assert_eq!(crate::class_ids::ALL.len(), 98); + assert_eq!(crate::class_ids::ALL.len(), 99); for (_, id) in crate::class_ids::ALL { assert_ne!(*id >> 8, 0x17, "a 0x17XX row reached the codebook"); } diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 353d8196..f648436f 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1345,6 +1345,7 @@ const CODEBOOK: &[(&str, u16)] = &[ ("auth_zitadel", 0x0B02), ("auth_zanzibar", 0x0B03), ("auth_ory_keto", 0x0B04), + ("auth_surrealdb", 0x0B05), // ── 0x0CXX — Automation domain (the HIRO IT-automation stack) ── // One domain spanning the MARS structural CMDB (`ogit.MARS:` — // Application/Resource/Software/Machine, the A→R→S→M dependsOn backbone) @@ -1494,7 +1495,7 @@ pub enum ConceptDomain { Anatomy, /// `0x0BXX` — Auth (IAM; provider-agnostic — the AuthStore class /// family: `auth_store` + per-IdP profiles `auth_zitadel` / - /// `auth_zanzibar` / `auth_ory_keto`). See + /// `auth_zanzibar` / `auth_ory_keto` / `auth_surrealdb`). See /// `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` §7. Auth, /// `0x0CXX` — Automation (the HIRO IT-automation stack). One domain @@ -2202,6 +2203,11 @@ pub mod class_ids { pub const AUTH_ZANZIBAR: u16 = 0x0B03; /// `auth_ory_keto` (`0x0B04`) — Ory Keto provider profile. pub const AUTH_ORY_KETO: u16 = 0x0B04; + /// `auth_surrealdb` (`0x0B05`) — SurrealDB IAM provider profile: + /// system users and record access, Viewer / Editor / Owner roles bound to + /// a level (root ⊃ namespace ⊃ database ⊃ record). Claims `ID` / `RL` / + /// `NS` (+ `DB`, `AC`). Harvested in `crates/ogar-auth-surrealdb`. + pub const AUTH_SURREALDB: u16 = 0x0B05; // ── 0x0DXX — HR domain (employment / org / contracts) ── @@ -2407,6 +2413,7 @@ pub mod class_ids { ("auth_zitadel", AUTH_ZITADEL), ("auth_zanzibar", AUTH_ZANZIBAR), ("auth_ory_keto", AUTH_ORY_KETO), + ("auth_surrealdb", AUTH_SURREALDB), // 0x0DXX — HR (employment / org / contracts; closes the final // 4-of-11 cross-axis gap from odoo-rs PR #14) ("hr_employee", HR_EMPLOYEE), @@ -2504,7 +2511,7 @@ pub mod class_ids { // Pin the number here so a bump is never silent. assert_eq!( ALL.len(), - 98, + 99, "class_ids::ALL count changed — update this pin AND land the \ corresponding row in lance-graph's \ crates/lance-graph-contract/src/ogar_codebook.rs::CODEBOOK \ @@ -3367,6 +3374,7 @@ pub fn all_promoted_classes() -> Vec { auth_zitadel(), auth_zanzibar(), auth_ory_keto(), + auth_surrealdb(), // 0x0DXX — HR arm hr_employee(), hr_department(), @@ -4863,6 +4871,15 @@ pub fn auth_ory_keto() -> Class { auth_provider("AuthOryKeto", "auth_ory_keto") } +/// The `auth_surrealdb` (`0x0B05`) provider profile — SurrealDB IAM. Claims: +/// subject `ID`, roles `RL`, namespace `NS`; `DB` and `AC` narrow the scope. +/// Roles are bound to a level, which enters authorization as a nested scope +/// path, not as the tenant. +#[must_use] +pub fn auth_surrealdb() -> Class { + auth_provider("AuthSurrealDb", "auth_surrealdb") +} + // ── 0x0AXX — Anatomy domain builders (FMA reference kinds) ── // // The public anatomical reference frame consumed by the splat-native arc @@ -6040,6 +6057,7 @@ mod tests { assert_eq!(canonical_concept_domain(0x0900), ConceptDomain::Health); assert_eq!(canonical_concept_domain(0x0B00), ConceptDomain::Auth); assert_eq!(canonical_concept_domain(0x0B04), ConceptDomain::Auth); + assert_eq!(canonical_concept_domain(0x0B05), ConceptDomain::Auth); // Anatomy block (0x0A) — FMA reference kinds. assert_eq!(canonical_concept_domain(0x0A00), ConceptDomain::Anatomy); assert_eq!(canonical_concept_domain(0x0A03), ConceptDomain::Anatomy); @@ -6122,6 +6140,7 @@ mod tests { ("auth_zitadel", 0x0B02), ("auth_zanzibar", 0x0B03), ("auth_ory_keto", 0x0B04), + ("auth_surrealdb", 0x0B05), ] { assert_eq!( canonical_concept_id(concept), @@ -6130,8 +6149,8 @@ mod tests { ); assert_eq!(canonical_concept_domain(id), ConceptDomain::Auth); } - // The four preminted profiles are the whole Auth block today. - assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 4); + // The base and its four provider profiles are the whole Auth block. + assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 5); } #[test] @@ -6282,7 +6301,7 @@ mod tests { assert_eq!(concepts_in_domain(ConceptDomain::Commerce).count(), 11); assert_eq!(concepts_in_domain(ConceptDomain::ProjectMgmt).count(), 26); assert_eq!(concepts_in_domain(ConceptDomain::Anatomy).count(), 4); - assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 4); + assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 5); assert_eq!(concepts_in_domain(ConceptDomain::Automation).count(), 9); // Every yielded Automation id really is in-domain (0x0CXX). let automation: Vec<&str> = concepts_in_domain(ConceptDomain::Automation) From 8e9b87130b4214550aca15f0437a043386e2a174 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 00:39:47 +0000 Subject: [PATCH 2/4] ogar-rbac: GrantSource::scope_of carries axis-3 row scope to the kernel A provider-agnostic hook, default None (global), so every existing source is unchanged. OgarRbac::row_scope delegates to it; authorize_scoped folds the scopes of the granting roles. Needs ScopePath from lance-graph (contract::rbac), so this lands after the paired lance-graph PR. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- crates/ogar-rbac/src/lib.rs | 56 ++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/crates/ogar-rbac/src/lib.rs b/crates/ogar-rbac/src/lib.rs index d39ca94f..cf6824f1 100644 --- a/crates/ogar-rbac/src/lib.rs +++ b/crates/ogar-rbac/src/lib.rs @@ -56,7 +56,7 @@ #![warn(missing_docs)] use lance_graph_contract::rbac::{ - ActorId, ClassGrant, ClassId, ClassRbac, Operation, RoleId, grants_permit, + ActorId, ClassGrant, ClassId, ClassRbac, Operation, RoleId, ScopeSpec, grants_permit, }; use lance_graph_rbac::authorize::{ScopedDecision, authorize_scoped}; use ogar_auth::user::AuthenticatedUser; @@ -74,6 +74,14 @@ pub trait GrantSource { /// The typed `granted` set of `role` — its `(target_classid, op_mask)` pairs. fn grants_of(&self, role: RoleId) -> &[ClassGrant]; + + /// Where `role`'s grants on `class` apply — the axis-3 row scope, including + /// a nested [`ScopePath`](lance_graph_contract::rbac::ScopePath) when roles + /// are bound to a level of a hierarchy (a namespace, a database, an org). + /// `None` (the default) is global: every existing source is unchanged. + fn scope_of(&self, _role: RoleId, _class: ClassId) -> Option { + None + } } /// OGAR's canonical [`ClassRbac`] authority. @@ -130,9 +138,12 @@ impl ClassRbac for OgarRbac { fn grant_permits(&self, role: RoleId, class: ClassId, op: &Operation<'_>) -> bool { grants_permit(self.source.grants_of(role), class, op) } - // Axes 2/3/4 (`roles_reaching` / `row_scope` / `field_mask`) inherit the - // contract defaults until the Core carries the data for them — a follow-up - // seam, not this patch. `field_mask`'s default is now WideFieldMask, so a + + fn row_scope(&self, role: RoleId, class: ClassId) -> Option { + self.source.scope_of(role, class) + } + // Axes 2/4 (`roles_reaching` / `field_mask`) inherit the contract defaults + // until the Core carries the data for them — a follow-up seam. `field_mask`'s default is now WideFieldMask, so a // grant on a position >= 64 survives once a source supplies one. } @@ -386,4 +397,41 @@ mod tests { ); assert_eq!(d.field_mask.count(), 3); } + + /// Axis 3 reaches the decision: a source that binds a role to a level of a + /// hierarchy gets that scope back on the `Allow`, and a source that does not + /// stays global. The scope travels; the kernel does not interpret it. + #[test] + fn a_sources_row_scope_travels_with_the_decision() { + use lance_graph_contract::rbac::{ScopePath, ScopeSpec}; + + struct Scoped(Fixture); + impl GrantSource for Scoped { + fn roles_of(&self, actor: ActorId<'_>) -> &[RoleId] { + self.0.roles_of(actor) + } + fn grants_of(&self, role: RoleId) -> &[ClassGrant] { + self.0.grants_of(role) + } + fn scope_of(&self, role: RoleId, _class: ClassId) -> Option { + (role == "physician").then(|| ScopeSpec { + path: ScopePath::new(&[3, 1]).expect("depth"), + ..ScopeSpec::default() + }) + } + } + + let read = || Operation::Read { + depth: PrefetchDepth::Identity, + }; + let scoped = OgarRbac::new(Scoped(authority().source)); + let d = authorize_scoped(&scoped, "dr-house", patient_class(), read()); + assert_eq!(d.decision, AccessDecision::Allow); + let scope = d.scope.expect("a bound role yields a scope"); + assert!(scope.admits(&ScopePath::new(&[3, 1, 9]).expect("depth"))); + assert!(!scope.admits(&ScopePath::new(&[3, 2]).expect("depth"))); + + let global = authorize_scoped(&authority(), "dr-house", patient_class(), read()); + assert_eq!(global.scope, None, "a source without scopes stays global"); + } } From 0e891ef7499e1f2e90cf22dcf1583372d7ff70d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 00:40:25 +0000 Subject: [PATCH 3/4] docs: harvest SurrealDB core IAM onto the RBAC keystone SurrealDB's authorization (levels, Viewer/Editor/Owner, View/Edit, 21 resource kinds, the is_allowed_check rules, the ID/RL/NS/DB/AC claims) mapped onto the four axes, with nested levels as ScopePath on axis 3. Records what landed and the four things that still block the bit-for-bit equivalence probe. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- .claude/board/LATEST_STATE.md | 2 +- docs/SURREALDB-IAM-HARVEST.md | 83 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 docs/SURREALDB-IAM-HARVEST.md diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 11d97ce7..d6ddc6fb 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -83,7 +83,7 @@ table drops for brevity. | `0x08` | Ocr | Optical character recognition / document extraction. | | `0x09` | Health | Clinical/patient/care (PHI). | | `0x0A` | Anatomy | FMA reference ontology — public structure, distinct from `Health` (a finding *about* the structure is PHI, the structure itself is not). | -| `0x0B` | Auth | IAM, provider-agnostic — AuthStore class family (`auth_store` + per-IdP profiles). See `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` §7. | +| `0x0B` | Auth | IAM, provider-agnostic — AuthStore class family (`auth_store` + per-IdP profiles: Zitadel, Zanzibar, Ory Keto, SurrealDB `0x0B05`). See `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` §7 and `docs/SURREALDB-IAM-HARVEST.md`. | | `0x0C` | Automation | HIRO IT-automation — MARS CMDB + Automation actuators (THINK+DO meet here). | | `0x0D` | HR | Employment/org/contracts, public master-data. | | `0x0E` | Genetics | CPIC pharmacogenomics, consumed by q2. Zero shared-vocab rows (reserved posture); V3 marker form `0x0E01_1000`. | diff --git a/docs/SURREALDB-IAM-HARVEST.md b/docs/SURREALDB-IAM-HARVEST.md new file mode 100644 index 00000000..c78d1773 --- /dev/null +++ b/docs/SURREALDB-IAM-HARVEST.md @@ -0,0 +1,83 @@ +# SurrealDB core IAM — harvest onto the classid RBAC keystone + +Source: `AdaWorldAPI/surrealdb` at `ce1b04a`, `surrealdb/core/src/iam/`. +Target: `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` (the four axes, `ClassRbac`, +`authorize_scoped`). Profile: `auth_surrealdb` (`0x0B05`), is-a `auth_store`. + +## Why SurrealDB + +The keystone's stage 2 (row scope) has no scope-bearing reference proven yet +(§10: "the keystone stays CONJECTURE as a whole until a scope-bearing reference +is green"). SurrealDB's authorization is small, deterministic, and scoped by a +**nested** level, so it is a usable reference for axis 3. + +## What SurrealDB decides + +About 600 of the module's 10.6k lines are authorization; the rest +(`signin.rs`, `signup.rs`, `verify.rs`, `jwks.rs`, `token.rs`) is +authentication and belongs to the membrane. + +| SurrealDB | file | meaning | +|---|---|---| +| `Level::{No, Root, Namespace(ns), Database(ns, db), Record(ns, db, ac)}` | `entities/resources/level.rs` | where an actor or a resource sits; `sublevel_of` is containment, every level contains itself | +| `Role::{Viewer, Editor, Owner}` | `entities/roles.rs` | predefined; custom roles are a declared TODO upstream | +| `Action::{View, Edit}` | `entities/action.rs` | `SELECT`/`LIVE`/`SHOW` → View; writes and `ACCESS` → Edit | +| `ResourceKind` (21 kinds incl. `Config(_)`, `Actor`) | `entities/resources/resource.rs` | what is acted on | +| `Actor { res: Resource, roles }` | `entities/resources/actor.rs` | an actor is a resource of kind `Actor` at a level | +| `is_allowed_check` | `mod.rs` | the whole decision | +| claims `ID`, `RL`, `NS`, `DB`, `AC` | `token.rs` | subject, roles, namespace, database, access method | + +The decision: + +- **View**: allowed iff the resource's level lies inside the actor's level. No + role is required. +- **Edit with `Owner`**: allowed iff the resource's level lies inside the + actor's level. +- **Edit with `Editor`**: as for Owner, but only for 12 kinds — Namespace, + Database, Record, Table, Document, Option, Function, Analyzer, Parameter, + Event, Field, Index. Any, Module, Model, Access, Config, Api, Bucket, + Sequence and Actor stay Owner-only. +- **Edit otherwise**: denied. + +`AuthLimit` / `Actor::new_limited` narrows an actor for a sub-operation: the +level moves down when the limit lies inside it, roles above the limit's role +are dropped, and an emptied role set becomes the limit's role or `Viewer`. + +## Mapping onto the four axes + +| axis | SurrealDB | OGAR | +|---|---|---| +| 1 class-grant (verb × class) | Action × ResourceKind, per role | `ClassGrant { target_classid, op_mask }` per role; View → `READ`, Edit → `WRITE` | +| 2 role hierarchy | Owner ⊇ Editor ⊇ Viewer only through `has_*_role` helpers; `is_allowed_check` itself tests roles individually | explicit grants per role (the kernel does not fold `roles_reaching`) | +| 3 row scope | the actor's level; containment by `sublevel_of` | `ScopeSpec.path` (`ScopePath`, lance-graph contract): Root = `ROOT`, `Namespace(a)` = `[a]`, `Database(a, x)` = `[a, x]`, `Record(a, x, ac)` = `[a, x, ac]`, segments interned; containment = `ScopeSpec::admits` | +| 4 field projection | none | full mask | + +Roleless actors: a record-level user holds no roles yet may View. It maps to an +explicit role granted View, as data in this profile, so the kernel keeps +"no roles ⇒ deny". + +## What landed + +- `auth_surrealdb` (`0x0B05`) in `ogar-vocab` + `ogar-class-view`; lance-graph + mirror row and `AuthProvider::SurrealDb` (grammar `ID` / `RL` / `NS`). +- `contract::rbac::ScopePath`, `ScopeSpec.path`, `ScopeSpec::admits`, path-aware + `intersect` (lance-graph). +- `GrantSource::scope_of` in `ogar-rbac`, so a source supplies axis-3 scope. + +## OPEN — what blocks the bit-for-bit equivalence probe + +1. **Resource kinds have no classids.** Grants key on codebook concepts, and the + codebook has no database-catalog concepts (namespace, table, field, index, + function, …). The Editor rule depends on the kind, so a probe needs either + minted catalog concepts or another way to carry the kind. +2. **Scope is bound to a membership, the trait binds it to a role.** + `ClassRbac::row_scope(role, class)` has no actor, and `RoleId` is + `&'static str`. A SurrealDB actor holds "Owner at namespace `a`": the scope + belongs to the (actor, role, level) membership. Expressing that today needs + one role id per (role, level) pair created at runtime, which `&'static str` + does not allow without leaking. Zitadel organisations and any org-scoped + role meet the same limit. +3. **`Level::No`** (anonymous) is contained by every level as a resource and + contains only itself as an actor. No `ScopePath` has that property; it is + left outside the probe. +4. **`AuthLimit` narrowing** is not modelled yet. From 583638c64258eb04654884df0fba58817dda76f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 05:13:48 +0000 Subject: [PATCH 4/4] Remove the auth_surrealdb profile and the SurrealDB write-up SurrealDB was meant as a pattern to study, not as an auth provider of this stack. Drops the 0x0B05 mint, its class-view row, the count pins and the harvest doc. The generic GrantSource::scope_of hook stays. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg --- .claude/board/LATEST_STATE.md | 2 +- crates/ogar-class-view/src/lib.rs | 2 - crates/ogar-vocab/src/capability_registry.rs | 2 +- crates/ogar-vocab/src/lib.rs | 29 ++----- docs/SURREALDB-IAM-HARVEST.md | 83 -------------------- 5 files changed, 7 insertions(+), 111 deletions(-) delete mode 100644 docs/SURREALDB-IAM-HARVEST.md diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index d6ddc6fb..11d97ce7 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -83,7 +83,7 @@ table drops for brevity. | `0x08` | Ocr | Optical character recognition / document extraction. | | `0x09` | Health | Clinical/patient/care (PHI). | | `0x0A` | Anatomy | FMA reference ontology — public structure, distinct from `Health` (a finding *about* the structure is PHI, the structure itself is not). | -| `0x0B` | Auth | IAM, provider-agnostic — AuthStore class family (`auth_store` + per-IdP profiles: Zitadel, Zanzibar, Ory Keto, SurrealDB `0x0B05`). See `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` §7 and `docs/SURREALDB-IAM-HARVEST.md`. | +| `0x0B` | Auth | IAM, provider-agnostic — AuthStore class family (`auth_store` + per-IdP profiles). See `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` §7. | | `0x0C` | Automation | HIRO IT-automation — MARS CMDB + Automation actuators (THINK+DO meet here). | | `0x0D` | HR | Employment/org/contracts, public master-data. | | `0x0E` | Genetics | CPIC pharmacogenomics, consumed by q2. Zero shared-vocab rows (reserved posture); V3 marker form `0x0E01_1000`. | diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index 016ca88a..4416432f 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -73,7 +73,6 @@ use ogar_vocab::{ anatomical_structure, auth_ory_keto, auth_store, - auth_surrealdb, auth_zanzibar, auth_zitadel, automation_trigger, @@ -268,7 +267,6 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("auth_zitadel", auth_zitadel()), ("auth_zanzibar", auth_zanzibar()), ("auth_ory_keto", auth_ory_keto()), - ("auth_surrealdb", auth_surrealdb()), // ── 0x0DXX — HR cluster (closes the final 4-of-11 odoo-rs #14 gap) ── ("hr_employee", hr_employee()), ("hr_department", hr_department()), diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index ae1d24af..c477b5c8 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -377,7 +377,7 @@ fn resolve_concept_row(id: u16) -> Option<(&'static str, u16)> { mod the_canon_carries_no_palette_rows { #[test] fn no_0x17xx_row_reached_the_globally_mirrored_codebook() { - assert_eq!(crate::class_ids::ALL.len(), 99); + assert_eq!(crate::class_ids::ALL.len(), 98); for (_, id) in crate::class_ids::ALL { assert_ne!(*id >> 8, 0x17, "a 0x17XX row reached the codebook"); } diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index f648436f..353d8196 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1345,7 +1345,6 @@ const CODEBOOK: &[(&str, u16)] = &[ ("auth_zitadel", 0x0B02), ("auth_zanzibar", 0x0B03), ("auth_ory_keto", 0x0B04), - ("auth_surrealdb", 0x0B05), // ── 0x0CXX — Automation domain (the HIRO IT-automation stack) ── // One domain spanning the MARS structural CMDB (`ogit.MARS:` — // Application/Resource/Software/Machine, the A→R→S→M dependsOn backbone) @@ -1495,7 +1494,7 @@ pub enum ConceptDomain { Anatomy, /// `0x0BXX` — Auth (IAM; provider-agnostic — the AuthStore class /// family: `auth_store` + per-IdP profiles `auth_zitadel` / - /// `auth_zanzibar` / `auth_ory_keto` / `auth_surrealdb`). See + /// `auth_zanzibar` / `auth_ory_keto`). See /// `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` §7. Auth, /// `0x0CXX` — Automation (the HIRO IT-automation stack). One domain @@ -2203,11 +2202,6 @@ pub mod class_ids { pub const AUTH_ZANZIBAR: u16 = 0x0B03; /// `auth_ory_keto` (`0x0B04`) — Ory Keto provider profile. pub const AUTH_ORY_KETO: u16 = 0x0B04; - /// `auth_surrealdb` (`0x0B05`) — SurrealDB IAM provider profile: - /// system users and record access, Viewer / Editor / Owner roles bound to - /// a level (root ⊃ namespace ⊃ database ⊃ record). Claims `ID` / `RL` / - /// `NS` (+ `DB`, `AC`). Harvested in `crates/ogar-auth-surrealdb`. - pub const AUTH_SURREALDB: u16 = 0x0B05; // ── 0x0DXX — HR domain (employment / org / contracts) ── @@ -2413,7 +2407,6 @@ pub mod class_ids { ("auth_zitadel", AUTH_ZITADEL), ("auth_zanzibar", AUTH_ZANZIBAR), ("auth_ory_keto", AUTH_ORY_KETO), - ("auth_surrealdb", AUTH_SURREALDB), // 0x0DXX — HR (employment / org / contracts; closes the final // 4-of-11 cross-axis gap from odoo-rs PR #14) ("hr_employee", HR_EMPLOYEE), @@ -2511,7 +2504,7 @@ pub mod class_ids { // Pin the number here so a bump is never silent. assert_eq!( ALL.len(), - 99, + 98, "class_ids::ALL count changed — update this pin AND land the \ corresponding row in lance-graph's \ crates/lance-graph-contract/src/ogar_codebook.rs::CODEBOOK \ @@ -3374,7 +3367,6 @@ pub fn all_promoted_classes() -> Vec { auth_zitadel(), auth_zanzibar(), auth_ory_keto(), - auth_surrealdb(), // 0x0DXX — HR arm hr_employee(), hr_department(), @@ -4871,15 +4863,6 @@ pub fn auth_ory_keto() -> Class { auth_provider("AuthOryKeto", "auth_ory_keto") } -/// The `auth_surrealdb` (`0x0B05`) provider profile — SurrealDB IAM. Claims: -/// subject `ID`, roles `RL`, namespace `NS`; `DB` and `AC` narrow the scope. -/// Roles are bound to a level, which enters authorization as a nested scope -/// path, not as the tenant. -#[must_use] -pub fn auth_surrealdb() -> Class { - auth_provider("AuthSurrealDb", "auth_surrealdb") -} - // ── 0x0AXX — Anatomy domain builders (FMA reference kinds) ── // // The public anatomical reference frame consumed by the splat-native arc @@ -6057,7 +6040,6 @@ mod tests { assert_eq!(canonical_concept_domain(0x0900), ConceptDomain::Health); assert_eq!(canonical_concept_domain(0x0B00), ConceptDomain::Auth); assert_eq!(canonical_concept_domain(0x0B04), ConceptDomain::Auth); - assert_eq!(canonical_concept_domain(0x0B05), ConceptDomain::Auth); // Anatomy block (0x0A) — FMA reference kinds. assert_eq!(canonical_concept_domain(0x0A00), ConceptDomain::Anatomy); assert_eq!(canonical_concept_domain(0x0A03), ConceptDomain::Anatomy); @@ -6140,7 +6122,6 @@ mod tests { ("auth_zitadel", 0x0B02), ("auth_zanzibar", 0x0B03), ("auth_ory_keto", 0x0B04), - ("auth_surrealdb", 0x0B05), ] { assert_eq!( canonical_concept_id(concept), @@ -6149,8 +6130,8 @@ mod tests { ); assert_eq!(canonical_concept_domain(id), ConceptDomain::Auth); } - // The base and its four provider profiles are the whole Auth block. - assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 5); + // The four preminted profiles are the whole Auth block today. + assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 4); } #[test] @@ -6301,7 +6282,7 @@ mod tests { assert_eq!(concepts_in_domain(ConceptDomain::Commerce).count(), 11); assert_eq!(concepts_in_domain(ConceptDomain::ProjectMgmt).count(), 26); assert_eq!(concepts_in_domain(ConceptDomain::Anatomy).count(), 4); - assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 5); + assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 4); assert_eq!(concepts_in_domain(ConceptDomain::Automation).count(), 9); // Every yielded Automation id really is in-domain (0x0CXX). let automation: Vec<&str> = concepts_in_domain(ConceptDomain::Automation) diff --git a/docs/SURREALDB-IAM-HARVEST.md b/docs/SURREALDB-IAM-HARVEST.md deleted file mode 100644 index c78d1773..00000000 --- a/docs/SURREALDB-IAM-HARVEST.md +++ /dev/null @@ -1,83 +0,0 @@ -# SurrealDB core IAM — harvest onto the classid RBAC keystone - -Source: `AdaWorldAPI/surrealdb` at `ce1b04a`, `surrealdb/core/src/iam/`. -Target: `docs/CLASSID-RBAC-KEYSTONE-SPEC.md` (the four axes, `ClassRbac`, -`authorize_scoped`). Profile: `auth_surrealdb` (`0x0B05`), is-a `auth_store`. - -## Why SurrealDB - -The keystone's stage 2 (row scope) has no scope-bearing reference proven yet -(§10: "the keystone stays CONJECTURE as a whole until a scope-bearing reference -is green"). SurrealDB's authorization is small, deterministic, and scoped by a -**nested** level, so it is a usable reference for axis 3. - -## What SurrealDB decides - -About 600 of the module's 10.6k lines are authorization; the rest -(`signin.rs`, `signup.rs`, `verify.rs`, `jwks.rs`, `token.rs`) is -authentication and belongs to the membrane. - -| SurrealDB | file | meaning | -|---|---|---| -| `Level::{No, Root, Namespace(ns), Database(ns, db), Record(ns, db, ac)}` | `entities/resources/level.rs` | where an actor or a resource sits; `sublevel_of` is containment, every level contains itself | -| `Role::{Viewer, Editor, Owner}` | `entities/roles.rs` | predefined; custom roles are a declared TODO upstream | -| `Action::{View, Edit}` | `entities/action.rs` | `SELECT`/`LIVE`/`SHOW` → View; writes and `ACCESS` → Edit | -| `ResourceKind` (21 kinds incl. `Config(_)`, `Actor`) | `entities/resources/resource.rs` | what is acted on | -| `Actor { res: Resource, roles }` | `entities/resources/actor.rs` | an actor is a resource of kind `Actor` at a level | -| `is_allowed_check` | `mod.rs` | the whole decision | -| claims `ID`, `RL`, `NS`, `DB`, `AC` | `token.rs` | subject, roles, namespace, database, access method | - -The decision: - -- **View**: allowed iff the resource's level lies inside the actor's level. No - role is required. -- **Edit with `Owner`**: allowed iff the resource's level lies inside the - actor's level. -- **Edit with `Editor`**: as for Owner, but only for 12 kinds — Namespace, - Database, Record, Table, Document, Option, Function, Analyzer, Parameter, - Event, Field, Index. Any, Module, Model, Access, Config, Api, Bucket, - Sequence and Actor stay Owner-only. -- **Edit otherwise**: denied. - -`AuthLimit` / `Actor::new_limited` narrows an actor for a sub-operation: the -level moves down when the limit lies inside it, roles above the limit's role -are dropped, and an emptied role set becomes the limit's role or `Viewer`. - -## Mapping onto the four axes - -| axis | SurrealDB | OGAR | -|---|---|---| -| 1 class-grant (verb × class) | Action × ResourceKind, per role | `ClassGrant { target_classid, op_mask }` per role; View → `READ`, Edit → `WRITE` | -| 2 role hierarchy | Owner ⊇ Editor ⊇ Viewer only through `has_*_role` helpers; `is_allowed_check` itself tests roles individually | explicit grants per role (the kernel does not fold `roles_reaching`) | -| 3 row scope | the actor's level; containment by `sublevel_of` | `ScopeSpec.path` (`ScopePath`, lance-graph contract): Root = `ROOT`, `Namespace(a)` = `[a]`, `Database(a, x)` = `[a, x]`, `Record(a, x, ac)` = `[a, x, ac]`, segments interned; containment = `ScopeSpec::admits` | -| 4 field projection | none | full mask | - -Roleless actors: a record-level user holds no roles yet may View. It maps to an -explicit role granted View, as data in this profile, so the kernel keeps -"no roles ⇒ deny". - -## What landed - -- `auth_surrealdb` (`0x0B05`) in `ogar-vocab` + `ogar-class-view`; lance-graph - mirror row and `AuthProvider::SurrealDb` (grammar `ID` / `RL` / `NS`). -- `contract::rbac::ScopePath`, `ScopeSpec.path`, `ScopeSpec::admits`, path-aware - `intersect` (lance-graph). -- `GrantSource::scope_of` in `ogar-rbac`, so a source supplies axis-3 scope. - -## OPEN — what blocks the bit-for-bit equivalence probe - -1. **Resource kinds have no classids.** Grants key on codebook concepts, and the - codebook has no database-catalog concepts (namespace, table, field, index, - function, …). The Editor rule depends on the kind, so a probe needs either - minted catalog concepts or another way to carry the kind. -2. **Scope is bound to a membership, the trait binds it to a role.** - `ClassRbac::row_scope(role, class)` has no actor, and `RoleId` is - `&'static str`. A SurrealDB actor holds "Owner at namespace `a`": the scope - belongs to the (actor, role, level) membership. Expressing that today needs - one role id per (role, level) pair created at runtime, which `&'static str` - does not allow without leaking. Zitadel organisations and any org-scoped - role meet the same limit. -3. **`Level::No`** (anonymous) is contained by every level as a resource and - contains only itself as an actor. No `ScopePath` has that property; it is - left outside the probe. -4. **`AuthLimit` narrowing** is not modelled yet.