Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/ogar-class-view/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ use ogar_vocab::{
crm_ticket,
currency_policy,
diagnosis,
directory_group,
directory_user,
document,
email,
examination,
external_practice,
group_membership,
hr_department,
hr_employee,
hr_employment_contract,
Expand All @@ -113,6 +116,7 @@ use ogar_vocab::{
joint,
knowledge_item,
lab_value,
license_assignment,
machine_memory_map,
mars_application,
mars_machine,
Expand Down Expand Up @@ -300,6 +304,10 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> {
("auth_zanzibar", auth_zanzibar()),
("auth_ory_keto", auth_ory_keto()),
("email", email()),
("directory_user", directory_user()),
("directory_group", directory_group()),
("group_membership", group_membership()),
("license_assignment", license_assignment()),
// ── 0x0DXX — HR cluster (closes the final 4-of-11 odoo-rs #14 gap) ──
("hr_employee", hr_employee()),
("hr_department", hr_department()),
Expand Down
5 changes: 2 additions & 3 deletions crates/ogar-flow-m365/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ fixture = []
[dependencies]
ogar-loco = { path = "../ogar-loco" }
ogar-auth = { path = "../ogar-auth" }
ogar-dir-core = { path = "../ogar-dir-core" }
# Zero-dep: the canonical concept ids the operations bind to.
ogar-vocab = { path = "../ogar-vocab" }
# Same coordinate the eight sibling OGAR crates and ogar-loco itself use.
lance-graph-contract = { git = "https://github.com/AdaWorldAPI/lance-graph", branch = "main" }

[dev-dependencies]
ogar-vocab = { path = "../ogar-vocab" }

[[example]]
name = "john_doe_101"
Expand Down
46 changes: 24 additions & 22 deletions crates/ogar-flow-m365/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,41 +107,43 @@ start with one source and end in `Export-Report`.
| Directory observations | **mocked**: `fixture::FixtureDirectory` (synthetic) |
| Report authority | **mocked**: `fixture::FixtureAuthority`, a test double, not security |
| Assertion verification | **mocked**: `fixture::FixtureVerifier`, driven through the real `establish` |
| Production authority via `ogar-rbac` (`OgarRbac::authorize_user`, field masks) | **deferred, gated** (below) |
| Observation adapter over `ogar-az` / Microsoft Graph | deferred (PR-FLOW-02) |
| Registering the vocabulary in a `VocabularyRegistry` under a body classid | **deferred, gated** (below) |
| Production authority via `ogar-rbac` (`OgarRbac::authorize_user`, field masks) | deferred (PR-FLOW-02); the classids it keys on now exist (below) |
| Observation adapter over `lance-graph-dir-sim` / `ogar-az` | deferred (PR-FLOW-02) |
| Registering the vocabulary in a `VocabularyRegistry` | implemented: `vocab::plug_into`, body concept `0x1718` |
| CSV / other export formats, `-like` / `-match`, `ForEach-Object`, sorting | not implemented |

### The two gates

Both are the same missing piece: **no directory-user, license-assignment,
group-membership or report concept is minted in `ogar-vocab`.** This crate
mints nothing.

- **RBAC.** `OgarRbac::authorize_user(identity, class, op)` and its field
masks are keyed by classid. Without a minted class for a directory user (and
for license and membership facts), the production authority has nothing to
be asked about. `ReportAuthority` is the seam it will implement.
- **Registry.** `VocabularyRegistry::plug` selects a vocabulary by the body
node's concept id. The flow vocabulary is validated and run directly; it is
not plugged into a registry, because that needs a body concept id (the
`ogar-ro` `0x0306` and `ogar-dismech` `0x0333` precedent), which is an
operator decision.
### Classids

Every operation records its binding (`adjacency::ConceptBinding`):
`DirSchema` (the `ogar-dir-core` MS Graph user schema), `Generic`, or
`Unminted("…")` naming what is missing.

| operations | binding |
|---|---|
| `query_user`, `update_user`, `remove_user` | `Minted(0x0B06)` `directory_user` |
| `read_membership`, `add_group_member` | `Minted(0x0B08)` `group_membership` |
| `read_license_evidence`, `assign_license` | `Minted(0x0B09)` `license_assignment` |
| `filter_rows`, `select_fields`, `evaluate_expression` | `Generic` |
| `export_report` | `Unminted`: report execution belongs to `lance-graph-report` |

`directory_user` (`0x0B06`), `directory_group` (`0x0B07`), `group_membership`
(`0x0B08`) and `license_assignment` (`0x0B09`) are minted in `ogar-vocab`'s
IAM domain, above `email`. They are what `OgarRbac` grants and field masks on
directory data key on, so `ReportAuthority` can be backed by it.

The flow body's concept id, `0x1718`, is a palette slot in `ogar-loco`'s
`0x17XX` domain, above `blockly-rs` (`0x1717`). Like every palette slot it is
declared in this crate, not in `ogar-vocab`'s mirrored codebook.
`vocab::plug_into` registers the vocabulary under it.

## The litmus test

`Get-MgUser` was added as a friendly label without changing anything in
`ogar-loco`: no new opcode, no change to the call ABI, the interpreter, the
conformance rules or `DOMAIN_FLOOR`. The only LOCO primitive missing for
production is outside LOCO: the concept ids above.
production was outside LOCO: the concept ids above.

## Falsifiers

`cargo test -p ogar-flow-m365` runs 29 tests over synthetic data, F1–F10:
`cargo test -p ogar-flow-m365` runs 31 tests over synthetic data, F1–F10:
resolution and refusal (F1), alias equivalence (F2), no new concept and
shared-core comparison reuse (F3), read-only enforcement including
hand-built bodies (F4), actor isolation: no scope, missing capability,
Expand Down
39 changes: 16 additions & 23 deletions crates/ogar-flow-m365/src/adjacency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
//! String matching happens here, once, at the compile boundary. Execution
//! sees only [`FnIndex`] bytes.

use ogar_dir_core::{SchemaFamily, SchemaId};
use ogar_loco::FnIndex;
use ogar_vocab::class_ids;

use crate::vocab;

Expand Down Expand Up @@ -105,19 +105,18 @@ pub enum Backend {

/// The semantic target an operation is bound to.
///
/// No variant carries a classid, because no directory-user, license,
/// group-membership or report concept is minted in `ogar-vocab` today, and
/// this crate does not mint concepts. Binding these to classids is the gate
/// for the production RBAC integration (README § Deferred).
/// A directory operation is bound to the `ogar-vocab` concept its rows are
/// (`directory_user`, `group_membership`, `license_assignment`), which is
/// what RBAC grants and field masks on that data key on.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConceptBinding {
/// A pipeline operator over any population (filter, projection). It has
/// no concept of its own.
Generic,
/// Bound to an existing typed directory schema in `ogar-dir-core`.
DirSchema(SchemaId),
/// The operation has no canonical OGAR concept yet. The string names the
/// missing piece so the gap is visible at the call site.
/// Bound to a concept minted in `ogar-vocab` (the hi-u16 concept id).
Minted(u16),
/// The operation has no OGAR concept. The string names why, so the gap is
/// visible at the call site.
Unminted(&'static str),
}

Expand Down Expand Up @@ -153,12 +152,6 @@ impl Operation {
}
}

/// The Graph `user` schema `ogar-az` encodes against.
const MS_GRAPH_USER: SchemaId = SchemaId {
family: SchemaFamily::MsGraph,
version: 2,
};

/// Every operation, indexed by [`OperationId`].
pub const OPERATIONS: &[Operation] = &[
// 0
Expand All @@ -170,7 +163,7 @@ pub const OPERATIONS: &[Operation] = &[
input: FlowShape::Nothing,
output: FlowShape::Population,
backend: Backend::Loco,
binding: ConceptBinding::DirSchema(MS_GRAPH_USER),
binding: ConceptBinding::Minted(class_ids::DIRECTORY_USER),
},
// 1
Operation {
Expand All @@ -181,7 +174,7 @@ pub const OPERATIONS: &[Operation] = &[
input: FlowShape::Population,
output: FlowShape::Population,
backend: Backend::Loco,
binding: ConceptBinding::Unminted("no license-assignment concept in ogar-vocab"),
binding: ConceptBinding::Minted(class_ids::LICENSE_ASSIGNMENT),
},
// 2
Operation {
Expand All @@ -192,7 +185,7 @@ pub const OPERATIONS: &[Operation] = &[
input: FlowShape::Population,
output: FlowShape::Population,
backend: Backend::Loco,
binding: ConceptBinding::Unminted("no directory group-membership concept in ogar-vocab"),
binding: ConceptBinding::Minted(class_ids::GROUP_MEMBERSHIP),
},
// 3
Operation {
Expand Down Expand Up @@ -226,7 +219,7 @@ pub const OPERATIONS: &[Operation] = &[
output: FlowShape::Report,
backend: Backend::Loco,
binding: ConceptBinding::Unminted(
"report execution is lance-graph-report's; no OGAR concept",
"report execution belongs to lance-graph-report; no OGAR concept",
),
},
// 6 — known, refused.
Expand All @@ -238,7 +231,7 @@ pub const OPERATIONS: &[Operation] = &[
input: FlowShape::Population,
output: FlowShape::Nothing,
backend: Backend::None,
binding: ConceptBinding::DirSchema(MS_GRAPH_USER),
binding: ConceptBinding::Minted(class_ids::DIRECTORY_USER),
},
// 7
Operation {
Expand All @@ -249,7 +242,7 @@ pub const OPERATIONS: &[Operation] = &[
input: FlowShape::Population,
output: FlowShape::Nothing,
backend: Backend::None,
binding: ConceptBinding::DirSchema(MS_GRAPH_USER),
binding: ConceptBinding::Minted(class_ids::DIRECTORY_USER),
},
// 8
Operation {
Expand All @@ -260,7 +253,7 @@ pub const OPERATIONS: &[Operation] = &[
input: FlowShape::Population,
output: FlowShape::Nothing,
backend: Backend::None,
binding: ConceptBinding::Unminted("no directory group-membership concept in ogar-vocab"),
binding: ConceptBinding::Minted(class_ids::GROUP_MEMBERSHIP),
},
// 9
Operation {
Expand All @@ -271,7 +264,7 @@ pub const OPERATIONS: &[Operation] = &[
input: FlowShape::Population,
output: FlowShape::Nothing,
backend: Backend::None,
binding: ConceptBinding::Unminted("no license-assignment concept in ogar-vocab"),
binding: ConceptBinding::Minted(class_ids::LICENSE_ASSIGNMENT),
},
// 10
Operation {
Expand Down
93 changes: 87 additions & 6 deletions crates/ogar-flow-m365/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,21 @@ fn f3_no_classid_is_minted_and_no_label_is_a_concept() {
"{}",
op.name
);
// Bindings name an existing schema, the generic operator, or the
// missing concept, never a classid of their own.
assert!(matches!(
op.binding,
ConceptBinding::Generic | ConceptBinding::DirSchema(_) | ConceptBinding::Unminted(_)
));
// A minted binding names a concept ogar-vocab actually mints, in the
// IAM domain; nothing here invents a classid of its own.
if let ConceptBinding::Minted(id) = op.binding {
assert!(
ogar_vocab::canonical_concept_name(id).is_some(),
"{}",
op.name
);
assert_eq!(
ogar_vocab::canonical_concept_domain(id),
ogar_vocab::ConceptDomain::Auth,
"{}",
op.name
);
}
}
for l in LABELS {
let normal = l.text.to_ascii_lowercase().replace('-', "_");
Expand Down Expand Up @@ -823,3 +832,75 @@ fn a_run_that_leaves_values_on_the_stack_returns_no_report() {
Err(RunError::Dialect(FlowError::NoReport))
);
}

#[test]
fn f3_directory_operations_bind_to_the_minted_directory_concepts() {
use ogar_vocab::class_ids;
let binding = |name: &str| {
OPERATIONS
.iter()
.find(|o| o.name == name)
.map(|o| o.binding)
.unwrap()
};
for (op, id, concept) in [
("query_user", class_ids::DIRECTORY_USER, "directory_user"),
(
"read_license_evidence",
class_ids::LICENSE_ASSIGNMENT,
"license_assignment",
),
(
"read_membership",
class_ids::GROUP_MEMBERSHIP,
"group_membership",
),
("update_user", class_ids::DIRECTORY_USER, "directory_user"),
(
"assign_license",
class_ids::LICENSE_ASSIGNMENT,
"license_assignment",
),
(
"add_group_member",
class_ids::GROUP_MEMBERSHIP,
"group_membership",
),
] {
assert_eq!(binding(op), ConceptBinding::Minted(id), "{op}");
assert_eq!(ogar_vocab::canonical_concept_name(id), Some(concept));
}
// Pipeline operators have no concept, and report execution has none here.
assert_eq!(binding("filter_rows"), ConceptBinding::Generic);
assert_eq!(binding("select_fields"), ConceptBinding::Generic);
assert!(matches!(
binding("export_report"),
ConceptBinding::Unminted(_)
));
}

#[test]
fn f9_the_flow_vocabulary_plugs_into_a_registry_under_its_body_classid() {
let mut hub = ogar_loco::VocabularyRegistry::new();
vocab::plug_into(&mut hub).unwrap();
let t = hub
.resolve_classid(vocab::flow_body_render_classid(0x0001))
.expect("0x1718 routes to the flow vocabulary");
assert_eq!(t, vocab::checked().table());
assert_eq!(t.name(vocab::GET_USER), Some("get_user"));
assert_eq!(
vocab::plug_into(&mut hub),
Err(ogar_loco::RegistryError::ConceptTaken {
concept_id: vocab::FLOW_BODY_CONCEPT_ID
})
);
// A palette slot: in LOCO's domain, and absent from the global codebook.
assert_eq!(
ogar_vocab::canonical_concept_domain(vocab::FLOW_BODY_CONCEPT_ID),
ogar_vocab::ConceptDomain::Blocks
);
assert_eq!(
ogar_vocab::canonical_concept_name(vocab::FLOW_BODY_CONCEPT_ID),
None
);
}
27 changes: 26 additions & 1 deletion crates/ogar-flow-m365/src/vocab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,36 @@
//! second operator table.

use lance_graph_contract::class_view::FieldMask;
use ogar_loco::{FnIndex, LaneShape, ValueCodebook, Vocabulary};
use ogar_loco::{FnIndex, LaneShape, RegistryError, ValueCodebook, Vocabulary, VocabularyRegistry};

/// Every flow body's lane carving.
pub const FLOW_SHAPE: LaneShape = LaneShape::Quads;

/// `0x1718` — the concept id of a flow body node: the classid a
/// [`VocabularyRegistry`] routes to [`FlowVocabulary`].
///
/// A palette slot in `ogar-loco`'s `0x17XX` domain, seated above
/// `blockly-rs` (`0x1717`) as `ogar-loco` reserves `0x1717`+ for consumer
/// palettes. Like `0x1717`, it lives in this crate, not in `ogar-vocab`'s
/// globally mirrored codebook (a palette is a frontend's private reading).
pub const FLOW_BODY_CONCEPT_ID: u16 = 0x1718;

/// The full render classid of a flow body under `app_prefix`.
#[must_use]
pub const fn flow_body_render_classid(app_prefix: u16) -> u32 {
((FLOW_BODY_CONCEPT_ID as u32) << 16) | (app_prefix as u32)
}

/// Register [`FlowVocabulary`] under [`FLOW_BODY_CONCEPT_ID`].
///
/// # Errors
///
/// [`RegistryError::ConceptTaken`] when the registry already has a vocabulary
/// for `0x1718`.
pub fn plug_into(registry: &mut VocabularyRegistry) -> Result<(), RegistryError> {
registry.plug(FLOW_BODY_CONCEPT_ID, &checked())
}

/// `get_user`: look up one directory user.
pub const GET_USER: FnIndex = FnIndex(0x90);
/// `get_license_evidence`: attach observed license facts.
Expand Down
Loading
Loading