Skip to content

release v4.1.3#1517

Open
bplatz wants to merge 13 commits into
mainfrom
release/v4.1.3
Open

release v4.1.3#1517
bplatz wants to merge 13 commits into
mainfrom
release/v4.1.3

Conversation

@bplatz

@bplatz bplatz commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Bumps the workspace version to 4.1.3 (root + testsuite-sparql + testsuite-shacl, with refreshed Cargo.locks).

Branched from the tip of feat/cli-model-access, so this release PR also carries the fluree model governance tooling (#1516). After merge, tag the merge commit v4.1.3 to trigger cargo-dist per docs/contributing/releasing.md.

bplatz added 13 commits July 8, 2026 11:03
First facet of the governance-model tooling (fluree model). Users
declare intent; the CLI compiles it to stored policies and transacts
them as ordinary data — no bespoke server API, so it works identically
against local ledgers, fluree-db-server, and hosted stacks.

fluree model access enable <dataset> --profile read|write|intake
  --entity <class-iri> [--property <iri>...] [--allow-shared]
  [--class-iri <iri>] [--dry-run] [--remote <r>]
fluree model access show <dataset>

Compilation: a policy class (the assignment unit grants/tokens carry),
a view policy (f:onClass — exact for reads), a property-whitelist
modify policy (f:onProperty + f:allow — class-targeted modify cannot
cover new-subject inserts and f:query evaluates pre-state), and a
declarative fm:AccessProfile node recording the intent so enable is
idempotent (upsert) and future sync/verify re-derive from it.

Property derivation: explicit → SHACL shape → observed data →
fail-closed. Uniqueness partition: properties used by other classes
are excluded by default with the blast radius disclosed
('also used by: Invoice'), included only with --allow-shared; every
run prints an exactness verdict (class-exact vs property-approximate)
and flags the rdf:type creation caveat.

Unit tests cover compilation shapes per profile, the parse gates, and
the IRI validation; verified end-to-end in local mode (enable → show →
identity+policy-class insert allowed → non-whitelisted write denied →
shared-property partition + disclosure).
…tacks

Completes the enable flow end-to-end: after the policies land in the
ledger (data plane), --space merges the compiled policy class into the
space's grant on the dataset via the stack's grants API — the one
system-plane touch in the compiler, since grants are router-owned
invariants (scope validation, membership checks).

- Reads existing grants first and merges classes (never clobbers);
  keeps the existing access level, upgrading read → write with a
  printed notice only when the profile requires writes.
- Policies-then-grant ordering is deliberate: a partial failure leaves
  unused policies (harmless), never a grant naming absent classes.
- Root URL derived from the remote's data-plane URL (…/v1/fluree);
  clear errors when the remote isn't a hosted stack or has no token.
- --space without --remote is a usage error.

Verified against a live stack: enable … --space wrote 4 policies +
attached the grant in one command; a minted space app token then
carried leads2 with the compiled class, a whitelisted insert passed,
and a non-whitelisted write was rejected at the ledger.
Terminology only: user-facing output now prints 'Allowed: N properties',
identifiers use allowed/allow_list, docs say property allow-list. No
behavior change.
Second facet of the governance-model tooling. The entity shape is the
model's single source of truth: the property list is written ONCE here
and everything else derives — model access enable now prefers
shape-derivation for its allow-lists, transaction-time validation
enforces the shape (the shapes-exist heuristic activates reject mode),
and SDK types / form fields can be generated from it later.

fluree model entity define <dataset> --entity <iri> [--label <l>]
  --property "<iri> [string|integer|decimal|boolean|date|datetime|iri]
              [required] [in[v1,v2,...]]" ...
fluree model entity show <dataset>

Compiles to an rdfs:Class node + a sh:NodeShape targeting it, with
stable property-shape ids ({entity}/shape/{localname}) so re-define is
an idempotent upsert. The define output states plainly that validation
becomes ACTIVE (existing data is not retro-validated; fluree validate
reports) and prints the model access enable synergy hint.

Shared ledger-IO helpers (resolve_mode/query/upsert) promoted from
access.rs to model/mod.rs.

Verified locally end-to-end: define (no seed data) → access enable
reports 'Derivation: shacl-shape' (class-exact, 3 properties) → a
shape-violating insert is rejected by SHACL with a violation report →
a conforming insert through the compiled policy class commits → both
show views render.
Third tier of the access compiler: a SPARQL property-path subset
(sequence / and inverse ^ of angle-bracketed absolute IRIs), anchored
identity → path → subject, expanded into f:query where-patterns on the
view policy (f:query replaces f:allow — they are alternative decision
modes). The raw path is recorded as fm:connected on the profile node.

  --connected "^<https://x/owner>"                 # I see what I own
  --connected "<https://x/memberOf>/^<https://x/team>"
                                    # I see entities whose team I'm in

v1 scope: --profile read only. Relationship gates evaluate against
pre-transaction state, so gating writes would deny every create (the
connection triples are not yet visible); staged-state evaluation at
the engine lifts this later. Alternatives (|) and transitive (+/*)
paths are rejected with a clear message until f:query supports them.

Verified live in local mode: two users on different teams, one enable
with --connected, and each user's query returns only their own team's
entities — graph-native row-level security from a single flag.
…nd hierarchy

- 'model access verify' recompiles every stored fm:AccessProfile from its
  intent node and semantically compares against the policies in the
  ledger (compact/full form tolerant). Hand-edited allow-lists, flipped
  f:allow, retargeted classes, and missing/unexpected policies report as
  DRIFT with a nonzero exit for CI.
- 'model access sync' re-derives profiles whose property surface came
  from a source that can change (SHACL shape, observed data) and
  recompiles on drift; explicit lists are never rewritten. The compiler
  now records fm:derivation and fm:allowShared on the intent node so
  sync honors the author's original choices; nodes written before these
  fields (or by other front ends) read as explicit — the safe default.
- 'model class define/show' — rdfs:subClassOf hierarchy as governed
  data, with the entailment blast-radius note; show excludes
  compiler-minted policy classes from the domain vocabulary.

Verified live against a hosted stack (verify caught a hand-widened
allow-list; enable repaired it) and locally (shape grew -> sync applied
'+score' -> verify clean).
Proves the storage form governance tooling uses for relationship gates:
a stored, classed view policy whose f:query is one triple over an @path
term (the author's path expression survives verbatim in the artifact),
selected via policy-class with a bind-only identity. Two identities,
one gate, each sees only entities connected via ex:memberOf/^ex:team.
…caffolder

The engine's f:create/f:update/f:delete verbs (with pre∪post class
targeting and class-scoped rdf:type matching) make thin class policies
exact, so the entire workaround layer is deleted: no property allow-list
derivation, no uniqueness partition / --allow-shared, no fm: intent
vocabulary, no sync, no drift-verify (1,461 → ~700 lines). Profiles map
directly: read → view; write → view + create/update/delete; intake →
create-only. --property remains as optional column narrowing
(f:onProperty conjunction). --connected now stores the SPARQL path
VERBATIM via the engine's @path context term (readable + reversible in
the artifact; show extracts it back).

model entity define gains --closed (sh:closed + rdf:type carve-out):
validation owns the property surface, policy owns state transitions.

Contract proven by integration test
scaffolder_write_profile_grants_class_ownership_only: the scaffolder's
byte-exact output, stored + selected via policy-class with bind-only
identity, allows create/update of the class and denies minting or
editing other classes. @path gate test switched to the CLI's
angle-bracket absolute-IRI storage form.
Property-merge upsert could leave a stale f:allow: true on a policy node
when re-running `model access enable --connected`; the policy loader
gives f:allow precedence over f:query, so the new relationship gate was
silently disabled and the policy stayed allow-all. Install compiled
artifacts through one update transaction that wildcard-deletes every
OWNED node id (delete-if-exists via optional patterns) and inserts the
fresh compilation.

- access: owns {class}/view and {class}/write on every run, so gating
  switches are exact and profile switches on the same class revoke the
  node the new profile no longer emits
- entity: owns the shape and its property-shape children (a dropped
  sh:closed/sh:minCount/sh:in no longer survives a re-run); the entity
  class node is shared authorship with `model class define` and stays
  additive
- class show: recognize policy classes by the extra @type on
  f:AccessPolicy nodes (the fm:AccessProfile intent vocabulary no longer
  exists); correct the --subclass-of re-run hint (replaces, not extends)
- integration test pins the CLI-emitted replace transaction end-to-end:
  an allow-all read profile switched to a --connected gate must narrow
Add docs/cli/model.md covering the three governance facets (access
profiles, SHACL entity shapes, class hierarchy), their compile-to-data
architecture, and the atomic replace semantics of re-running enable and
define. Index it in docs/SUMMARY.md and the CLI command table.
The access command's target restriction compiles to f:onClass, but the
flag was named --entity — which reads as subject-level targeting
(f:onSubject) to RDF users, and clashed with `model class define
--class` taking the SAME iri for the same concept. Rename:

- --entity  → --class        (the governed class; what data)
- --class-iri → --policy-class (the assignment unit grants/tokens carry;
  how a request selects its policy set — not a data restriction)

`model entity define --entity` is unchanged: there the flag names the
entity facet's own concept. Internal names and enable's output follow
(Policy class: line), and the docs spell out the three class-shaped
roles: --class restricts data, --property narrows columns, the policy
class selects the policy set.
@bplatz
bplatz requested review from aaj3f and zonotope July 17, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant