Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Policy in Queries

Query-time enforcement uses Fluree’s policy model to filter individual flakes during query execution. The query plan is the same regardless of policy — what changes is which flakes the engine returns. The application sees a query result; the policy filtering is invisible.

This page documents how query-time enforcement works, how patterns interact with the plan, and how to test policies from the CLI. For the policy node shape and combining algorithm, see the policy model reference. For the underlying concept, see Policy enforcement.

How query-time filtering works

When a query is executed against a PolicyContext:

  1. The engine resolves the request’s policy set: identity-driven f:policyClass lookups + any inline opts.policy array.
  2. The plan executes normally — same join order, same indices.
  3. Each flake the plan would emit is checked against the policies whose target matches it (f:onProperty, f:onClass, f:onSubject, or default for untargeted policies).
  4. A flake survives only if the combining algorithm approves it.
  5. Surviving flakes flow through the rest of the plan (joins, filters, aggregates) as normal.

Filtering is at the flake level — a single subject can appear in the result with some properties visible and others elided.

Worked example

Two users in a mydb:main ledger:

fluree insert '{
  "@context": {"schema": "http://schema.org/", "ex": "http://example.org/"},
  "@graph": [
    {"@id": "ex:alice", "schema:name": "Alice", "ex:role": "engineer", "ex:salary": 130000},
    {"@id": "ex:bob",   "schema:name": "Bob",   "ex:role": "manager",  "ex:salary": 155000}
  ]
}'

A required policy that hides ex:salary unless the requester is a manager:

fluree insert '{
  "@context": {"f": "https://ns.flur.ee/db#", "ex": "http://example.org/"},
  "@graph": [
    {
      "@id": "ex:salary-restriction",
      "@type": ["f:AccessPolicy", "ex:CorpPolicy"],
      "f:required": true,
      "f:onProperty": [{"@id": "ex:salary"}],
      "f:action": [{"@id": "f:view"}],
      "f:query": "{\"where\": {\"@id\": \"?$identity\", \"http://example.org/role\": \"manager\"}}"
    },
    {
      "@id": "ex:default-view",
      "@type": ["f:AccessPolicy", "ex:CorpPolicy"],
      "f:action": [{"@id": "f:view"}],
      "f:allow": true
    },
    {"@id": "ex:aliceIdentity", "f:policyClass": [{"@id": "ex:CorpPolicy"}], "ex:role": "engineer"},
    {"@id": "ex:bobIdentity",   "f:policyClass": [{"@id": "ex:CorpPolicy"}], "ex:role": "manager"}
  ]
}'

The same query, executed as different identities:

# As Bob (manager) — sees salaries
fluree query --as ex:bobIdentity --policy-class ex:CorpPolicy \
  'SELECT ?name ?salary WHERE { ?p <http://schema.org/name> ?name ; <http://example.org/salary> ?salary }'
# → Alice 130000, Bob 155000

# As Alice (engineer) — salary flakes filtered out
fluree query --as ex:aliceIdentity --policy-class ex:CorpPolicy \
  'SELECT ?name ?salary WHERE { ?p <http://schema.org/name> ?name ; <http://example.org/salary> ?salary }'
# → no results: the join requires ?salary which is filtered for Alice

To get Alice’s name back without the salary join, use OPTIONAL:

SELECT ?name ?salary WHERE {
  ?p <http://schema.org/name> ?name .
  OPTIONAL { ?p <http://example.org/salary> ?salary }
}

Now Alice sees both names, with ?salary unbound — exactly the behavior an application expects when a property is suppressed by policy.

Enforcement across query operations

Filtering happens wherever the plan reads flakes, so every query operation enforces the same per-flake policy — not just basic triple patterns:

  • Triple patterns / BGPs — the common case shown above; each matched flake is checked.
  • OPTIONAL, joins, aggregates, COUNT — these operate over already-filtered flakes, so a hidden flake is absent from joins and excluded from counts. SELECT (COUNT(?o) ...) WHERE { ?s ex:salary ?o } counts only the salary flakes the identity may view.
  • Property paths (?x :knows+ ?y, :reportsTo*, sequence/alternation) — traversal only follows edges the identity can view. A hidden :knows flake is not walked, so nodes reachable only through hidden edges do not appear in the result. You traverse exactly the graph you can see.
  • Full-text and vector search (embedded indexes) — a search hit is returned only if the identity can view the indexed content that produced the match. If the policy hides the indexed property’s flake for a subject, that subject is dropped from the search results — even when selecting only the hit id/score with no constraining pattern. (This applies to Fluree’s embedded search indexes; an external/dedicated search service enforces its own access controls.)

In every case the rule is the same: the engine never emits — or traverses, or counts, or returns as a search hit — a flake the identity is not allowed to see.

Reasoning (RDFS / OWL / datalog)

Reasoning and view policy compose, but the contract is specific:

  • OWL 2 QL rewrites the query and runs it through the normal scan path under your identity, so it is filtered exactly like any other query.
  • OWL 2 RL and datalog materialize derived facts into the query’s overlay. Those derived facts are filtered by the same per-flake view policy as base data — a derived flake you may not view is dropped just like a stored one.

What the engine does not do is trace a derived fact’s provenance: a derived flake is judged by its own (subject, predicate, object), not by the base facts it was computed from. So if a rule or ontology axiom re-expresses hidden data under a different, viewable predicate — e.g. ex:ssn rdfs:subPropertyOf ex:identifier, or a rule that writes ex:isHighEarner from a hidden ex:salary — the derived value can surface even though the source is hidden.

So when you enable reasoning under a non-root policy, your policy must cover the derived properties and classes. Either deny them explicitly, or run with default-allow: false so any predicate you did not explicitly allow — including reasoning-introduced ones — is hidden by default. Inline per-query ontologies (f:schemaSource) are subject to the same rule: any class/property they entail must be covered by your policy.

Query-time rule injection (the query’s rules field) is admin-only: under a non-root view policy, caller-supplied datalog rules are stripped before execution, because a rule with a viewable head could launder hidden data the policy author never anticipated. Database-stored rules (f:rule) and OWL/RDFS reasoning are administrator-controlled and continue to apply.

Targeting patterns

Property-level (f:onProperty)

Restricts a flake whose predicate matches:

{
  "@id": "ex:hide-ssn",
  "@type": ["f:AccessPolicy", "ex:CorpPolicy"],
  "f:required": true,
  "f:onProperty": [{"@id": "http://schema.org/ssn"}],
  "f:action": [{"@id": "f:view"}],
  "f:query": "{\"where\": {\"@id\": \"?$identity\", \"http://example.org/role\": \"hr\"}}"
}

Flakes whose predicate is not schema:ssn are unaffected by this policy.

Class-level (f:onClass)

Restricts flakes whose subject has one of the listed rdf:types:

{
  "@id": "ex:employee-data-only",
  "@type": ["f:AccessPolicy", "ex:CorpPolicy"],
  "f:required": true,
  "f:onClass": [{"@id": "http://example.org/Employee"}],
  "f:action": [{"@id": "f:view"}],
  "f:query": "{\"where\": {\"@id\": \"?$identity\", \"@type\": \"http://example.org/Employee\"}}"
}

Flakes about non-Employee subjects fall through to other policies.

Subject-level (f:onSubject)

Restricts flakes about specific subjects:

{
  "@id": "ex:hide-internal-doc",
  "@type": ["f:AccessPolicy", "ex:CorpPolicy"],
  "f:required": true,
  "f:onSubject": [{"@id": "http://example.org/secret-doc"}],
  "f:action": [{"@id": "f:view"}],
  "f:allow": false
}

Default (no targeting)

A policy with no f:onProperty / f:onClass / f:onSubject applies to every flake. Use sparingly — default policies are evaluated against every emitted flake, which is more expensive than targeted policies.

SPARQL queries

SPARQL queries have no opts block, so policy is delivered via headers:

curl -X POST 'http://localhost:8090/v1/fluree/query?ledger=mydb:main' \
  -H 'Content-Type: application/sparql-query' \
  -H "Authorization: Bearer $JWT" \
  -H 'fluree-identity: ex:aliceIdentity' \
  -H 'fluree-policy-class: ex:CorpPolicy' \
  -H 'fluree-default-allow: false' \
  -d 'SELECT ?name WHERE { ?p <http://schema.org/name> ?name }'

The full header set is documented in the policy model.

JSON-LD queries

JSON-LD queries put policy in opts:

{
  "from": "mydb:main",
  "select": ["?name", "?salary"],
  "where": [
    {"@id": "?p", "schema:name": "?name"},
    ["optional", {"@id": "?p", "ex:salary": "?salary"}]
  ],
  "opts": {
    "identity": "ex:aliceIdentity",
    "policy-class": ["ex:CorpPolicy"],
    "default-allow": false
  }
}

Inline policies, additional policy-values, and multiple policy-class entries all live under opts. The full vocabulary is in the policy model reference.

Multi-graph queries

Policies apply per-flake, regardless of which named graph the flake came from. A query that pulls from multiple from-named graphs sees a uniformly filtered result — there’s no per-graph policy override.

If different graphs need different policy regimes, use targeted policies (f:onClass for type-scoped restrictions, f:onSubject for explicit subject lists). For wholly separate access regimes, use separate ledgers.

Time-travel queries

Policy evaluation honors the query’s t. When you query --at a past t:

  • The policy set itself is resolved at that t (so retired policies still apply when you time-travel back to when they were live).
  • Identity attributes used in f:query are evaluated at that t.

This makes audit-style queries — “What could Alice see on 2024-06-15?” — directly expressible:

fluree query --as ex:aliceIdentity --policy-class ex:CorpPolicy --at 2024-06-15T00:00:00Z \
  'SELECT ?p ?o WHERE { <http://example.org/financial-report> ?p ?o }'

Performance considerations

Two phases: load the policy set once per request; apply it to each touched flake.

  • Target policies whenever possible. A policy with f:onProperty only runs against flakes whose predicate matches. Default policies (no targeting) run against every flake.
  • Keep f:query cheap. It runs once per flake-target. Lean on identity-side properties already loaded (@type, f:policyClass, role flags) rather than deep traversals.
  • Avoid deep recursion in f:query. Each level of indirection multiplies the per-flake cost.
  • Required policies short-circuit on the first failed gate. Required policies are AND gates — every one must grant. As soon as any required gate fails (an explicit deny, or an f:query returning no rows), the flake is denied and the remaining required policies are skipped.
  • Targeted policies keep fast paths fast. Cardinality and scalar-aggregate fast paths (e.g. a bare COUNT over a single predicate) still answer directly from index metadata when the active policy provably cannot restrict the scanned predicate — a policy that only targets ex:salary does not slow a COUNT over schema:name. A policy that could apply to the scanned predicate (including any default/subject-targeted policy) forces the per-flake filtered scan instead. This is another reason to prefer targeted policies.

For complex deployments, the explain plan shows whether a query is dominated by policy filtering and which policies contribute.

Testing policies from the CLI

The fluree CLI supports policy-enforced queries so you can verify that the policies you’ve configured filter results as expected — without writing any client code.

Flags

Available on fluree query (and on fluree insert, upsert, update for write-time enforcement):

FlagPurpose
--as <IRI>Execute as this identity. Resolves f:policyClass on the identity subject to collect applicable policies, and binds ?$identity.
--policy-class <IRI>Apply stored policies of the given class IRI. Repeatable. Narrows to the intersection with the identity’s policies, or applies directly without --as.
--default-allowAllow when no matching policy exists for the operation. Defaults to false (deny-by-default).

Workflow

  1. Transact your policy rules (and the identities with their f:policyClass assignments) into the ledger, using any of the normal insert / upsert / update commands.
  2. Re-run the same query as different identities to confirm results differ as the policies prescribe:
# Full result set (no policy enforcement)
fluree query 'SELECT ?name ?salary WHERE { ?p <http://schema.org/name> ?name ; <http://example.org/salary> ?salary }'

# As an HR user — should see all salaries
fluree query --as ex:hrIdentity --policy-class ex:CorpPolicy \
  'SELECT ?name ?salary WHERE { ?p <http://schema.org/name> ?name ; <http://example.org/salary> ?salary }'

# As a regular employee — policies should hide salary field
fluree query --as ex:engineerIdentity --policy-class ex:CorpPolicy \
  'SELECT ?name ?salary WHERE { ?p <http://schema.org/name> ?name ; <http://example.org/salary> ?salary }'

Local vs remote

The flags work in both modes:

  • Local (default, or with --direct): the CLI loads the ledger directly and applies policy via the in-process query engine.
  • Remote (with --remote <name>, or auto-routed through a running local server): the CLI sends the flags to the server as HTTP headers (fluree-identity, fluree-policy-class, fluree-default-allow) and, for JSON-LD bodies, also injects them into opts. Multi-value --policy-class rides through the body opts only; SPARQL transport is single-valued via the header.

Remote impersonation: how it’s authorized

When you run against a remote server with --as <iri>, the server treats the request as impersonation and gates it as follows:

  1. Your bearer token’s identity is resolved on the target ledger.
  2. If that identity has no f:policyClass assignments (the FoundNoPolicies outcome — your service account is unrestricted on this ledger), the server honors --as and runs the query as the target identity.
  3. If your bearer identity is itself policy-constrained (FoundWithPolicies) or unknown to this ledger (NotFound), the server force-overrides --as with your bearer identity. You see your own filtered view, not the target’s.

Each successful impersonation is logged at info level on the server:

policy impersonation: bearer=<svc-id> target=<as-iri> ledger=<name>

This is the standard service-account pattern: register your CLI/app-server identity in the ledger with no f:policyClass, and it gains the right to delegate to any end-user identity for testing or per-request enforcement. Assigning a policy class to that identity revokes the delegation right with no config change.

Limitations

  • Inline policy rules (opts.policy) and policy variable bindings (opts.policy-values) are not yet exposed as CLI flags — use a JSON-LD query body with an "opts" block when you need those.
  • For SPARQL queries against a remote, only --as, single-value --policy-class, and --default-allow are wired (via headers). Multi-value --policy-class works on JSON-LD only.
  • Proxy-mode servers fall back to the legacy non-impersonation behavior — the upstream server performs the impersonation check.