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

Programmatic Policy API (Rust)

This guide covers how to use Fluree’s policy system programmatically in Rust applications.

Overview

There are two main approaches to applying policies programmatically:

  1. Identity-based policies (wrap_identity_policy_view): Policies stored in the database and loaded via f:policyClass on an identity subject
  2. Inline policies (wrap_policy_view with opts.policy): Policies provided directly in the query/transaction options

Identity-Based Policy Lookup

The recommended approach for production systems. Policies are stored in the ledger and loaded dynamically based on the identity’s f:policyClass property.

Storing Policies in the Database

First, insert policies with types that will be referenced by identities:

#![allow(unused)]
fn main() {
let policies = json!({
    "@context": {
        "f": "https://ns.flur.ee/db#",
        "ex": "http://example.org/ns/",
        "schema": "http://schema.org/"
    },
    "@graph": [
        // Identity with policy class assignment
        {
            "@id": "http://example.org/identity/alice",
            "f:policyClass": [{"@id": "ex:EmployeePolicy"}],
            "ex:user": {"@id": "ex:alice"}
        },

        // SSN restriction policy - only see your own SSN
        {
            "@id": "ex:ssnRestriction",
            "@type": ["f:AccessPolicy", "ex:EmployeePolicy"],
            "f:required": true,
            "f:onProperty": [{"@id": "schema:ssn"}],
            "f:action": {"@id": "f:view"},
            "f:query": serde_json::to_string(&json!({
                "where": {
                    "@id": "?$identity",
                    "http://example.org/ns/user": {"@id": "?$this"}
                }
            })).unwrap()
        },

        // Default allow policy for other properties
        {
            "@id": "ex:defaultAllowView",
            "@type": ["f:AccessPolicy", "ex:EmployeePolicy"],
            "f:action": {"@id": "f:view"},
            "f:allow": true
        }
    ]
});

// Prefer the lazy Graph API for transactions
fluree.graph("mydb:main")
    .transact()
    .insert(&policies)
    .commit()
    .await?;
}

Using wrap_identity_policy_view

Create a policy-wrapped view using an identity IRI:

#![allow(unused)]
fn main() {
use fluree_db_api::{wrap_identity_policy_view, FlureeBuilder, GraphDb};

let fluree = FlureeBuilder::memory().build_memory();
let ledger = fluree.ledger("mydb:main").await?;

// Wrap the ledger with identity-based policy
let wrapped = wrap_identity_policy_view(
    &ledger,
    "http://example.org/identity/alice",  // identity IRI
    true  // default_allow: allow access when no policy matches
).await?;

// Check policy properties
assert!(!wrapped.is_root(), "Should not be root/unrestricted");

// Create a view with the policy applied, then query using the builder
let view = GraphDb::from_ledger_state(&ledger)
    .with_policy(std::sync::Arc::new(wrapped.policy().clone()));

let query = json!({
    "select": ["?s", "?ssn"],
    "where": {
        "@id": "?s",
        "@type": "ex:User",
        "schema:ssn": "?ssn"
    }
});

let result = view.query(&fluree)
    .jsonld(&query)
    .execute()
    .await?;
}

How Identity Lookup Works

When you call wrap_identity_policy_view:

  1. Fluree queries for policies via the identity’s f:policyClass:

    SELECT ?policy WHERE {
        <identity-iri> f:policyClass ?class .
        ?policy a ?class .
        ?policy a f:AccessPolicy .
    }
    
  2. Each matching policy’s properties are loaded (f:action, f:allow, f:query, f:onProperty, etc.)

  3. The ?$identity variable is automatically bound to the identity IRI for use in f:query policies

Inline Policies with policy-values

For cases where policies should not be stored in the database, use inline policies with explicit ?$identity binding.

QueryConnectionOptions Pattern

#![allow(unused)]
fn main() {
use fluree_db_api::{QueryConnectionOptions, wrap_policy_view};
use std::collections::HashMap;

let policy = json!([{
    "@id": "ex:inlineSsnPolicy",
    "f:required": true,
    "f:onProperty": [{"@id": "http://schema.org/ssn"}],
    "f:action": "f:view",
    "f:query": serde_json::to_string(&json!({
        "where": {
            "@id": "?$identity",
            "http://example.org/ns/user": {"@id": "?$this"}
        }
    })).unwrap()
}]);

let opts = QueryConnectionOptions {
    policy: Some(policy),
    policy_values: Some(HashMap::from([(
        "?$identity".to_string(),
        json!({"@id": "http://example.org/identity/alice"}),
    )])),
    default_allow: true,
    ..Default::default()
};

let wrapped = wrap_policy_view(&ledger, &opts).await?;
}

Using query_from with Inline Policy

For FROM-driven queries where policy options are embedded in the query body, use query_from():

#![allow(unused)]
fn main() {
let query = json!({
    "@context": {
        "ex": "http://example.org/ns/",
        "schema": "http://schema.org/"
    },
    "from": "mydb:main",
    "opts": {
        "default-allow": true,
        "policy": [{
            "@id": "inline-ssn-policy",
            "f:required": true,
            "f:onProperty": [{"@id": "http://schema.org/ssn"}],
            "f:action": "f:view",
            "f:query": serde_json::to_string(&json!({
                "where": {
                    "@id": "?$identity",
                    "http://example.org/ns/user": {"@id": "?$this"}
                }
            })).unwrap()
        }],
        "policy-values": {
            "?$identity": {"@id": "http://example.org/identity/alice"}
        }
    },
    "select": ["?s", "?ssn"],
    "where": {
        "@id": "?s",
        "@type": "ex:User",
        "schema:ssn": "?ssn"
    }
});

let result = fluree.query_from()
    .jsonld(&query)
    .execute()
    .await?;
}

Policy Options Precedence

When multiple policy options are provided, they follow this precedence:

PriorityOptionBehavior
1 (highest)opts.identityQuery f:policyClass policies, auto-bind ?$identity
2opts.policy_classQuery policies of specified types
3 (lowest)opts.policyUse inline policy JSON directly

Important: If opts.identity is set, inline opts.policy is ignored.

Policy Structure Reference

f:allow (Static Allow/Deny)

{
    "@id": "ex:allowAll",
    "@type": ["f:AccessPolicy", "ex:MyPolicyClass"],
    "f:action": {"@id": "f:view"},
    "f:allow": true
}

f:query (Dynamic Evaluation)

{
    "@id": "ex:ownerOnly",
    "@type": ["f:AccessPolicy", "ex:MyPolicyClass"],
    "f:action": {"@id": "f:view"},
    "f:onProperty": [{"@id": "schema:ssn"}],
    "f:required": true,
    "f:query": "{\"where\": {\"@id\": \"?$identity\", \"ex:user\": {\"@id\": \"?$this\"}}}"
}

Policy Properties

PropertyTypeDescription
f:actionf:view / f:modifyWhat action this policy applies to
f:allowbooleanStatic allow (true) or deny (false)
f:querystring (JSON)Query that must return results for access to be granted
f:onPropertyIRI(s)Restrict policy to specific properties
f:onSubjectIRI(s)Restrict policy to specific subjects
f:onClassIRI(s)Restrict policy to instances of specific classes
f:requiredbooleanIf true, this policy MUST allow for access to be granted
f:exMessagestringCustom error message when policy denies access

Special Variables

VariableBinding
?$identityThe identity IRI (from opts.identity or policy_values["?$identity"])
?$thisThe subject being accessed (for property-level policies)

Policy Combining Algorithm

When multiple policies match a flake, they are combined using Deny Overrides:

  1. If any matching policy explicitly denies (f:allow: false), access is denied
  2. If a targeted policy’s f:query returns false, access is denied (doesn’t fall through to Default policies)
  3. If any policy allows (f:allow: true or f:query returns true), access is granted
  4. If no policies match and default_allow is true → access is granted
  5. Otherwise, access is denied

Identity resolution is three-state: FoundWithPolicies (restrictions apply) → FoundNoPolicies (subject exists, no restrictions) → NotFound (subject absent, no restrictions). The three-state split determines whether a concrete identity SID is available to bind ?$identity in policy queries; it does not gate default_allow. An unknown identity with default_allow: true is granted access — this is the intended behavior for deployments where an application layer handles authorization and Fluree records signed transactions for provenance. Set default_allow: false for fail-closed behavior.

Important: Inline policies must use full IRIs (e.g., "http://schema.org/ssn"), not compact IRIs (e.g., "schema:ssn"). Compact IRIs in inline policies are not expanded.

Transactions with Policy

Policies can also be applied to transactions using the builder API:

#![allow(unused)]
fn main() {
use fluree_db_api::policy_builder;

let policy_ctx = policy_builder::build_policy_context_from_opts(
    &ledger.snapshot,
    ledger.novelty.as_ref(),
    Some(ledger.novelty.as_ref()),
    ledger.t(),
    &qc_opts,
    &[0], // default graph; use resolve_policy_source_g_ids() for config-driven graphs
).await?;

let txn = json!({
    "@context": {"ex": "http://example.org/ns/"},
    "insert": [
        {"@id": "ex:alice", "ex:data": "secret"}
    ]
});

// Use the transaction builder with policy
let result = fluree.graph("mydb:main")
    .transact()
    .update(&txn)
    .policy(policy_ctx)
    .commit()
    .await;

match result {
    Ok(txn_result) => println!("Transaction succeeded at t={}", txn_result.ledger.t()),
    Err(e) => println!("Policy denied: {}", e),
}
}

Historical Views with Policy

For time-travel queries with policy, load a historical graph and apply policy as a view overlay:

#![allow(unused)]
fn main() {
use fluree_db_api::{GraphDb, QueryConnectionOptions};

// Load a historical view
let graph = fluree.view_at_t("mydb:main", 100).await?;

// Apply policy to create a view
let policy_ctx = policy_builder::build_policy_context_from_opts(
    &ledger.snapshot,
    ledger.novelty.as_ref(),
    Some(ledger.novelty.as_ref()),
    ledger.t(),
    &opts,
    &[0],
).await?;

let view = graph.with_policy(std::sync::Arc::new(policy_ctx));

// Query the historical view with policy applied
let result = view.query(&fluree)
    .jsonld(&query)
    .execute()
    .await?;
}

API Reference

wrap_identity_policy_view

#![allow(unused)]
fn main() {
pub async fn wrap_identity_policy_view<'a>(
    ledger: &'a LedgerState,
    identity_iri: &str,
    default_allow: bool,
) -> Result<PolicyWrappedView<'a>>
}

Creates a policy-wrapped view using identity-based f:policyClass lookup.

Parameters:

  • ledger: The ledger state to wrap
  • identity_iri: IRI of the identity subject (will query f:policyClass)
  • default_allow: Whether to allow access when no policies match. Ignored (forced false) if the identity IRI has no subject node in the ledger — see combining algorithm step 5

wrap_policy_view

#![allow(unused)]
fn main() {
pub async fn wrap_policy_view<'a>(
    ledger: &'a LedgerState,
    opts: &QueryConnectionOptions,
) -> Result<PolicyWrappedView<'a>>
}

Creates a policy-wrapped view from query connection options.

QueryConnectionOptions fields:

  • identity: Identity IRI for f:policyClass lookup
  • policy: Inline policy JSON
  • policy_class: Policy class IRIs to query
  • policy_values: Variable bindings for policy queries
  • default_allow: Default access when no policies match

PolicyWrappedView

#![allow(unused)]
fn main() {
impl PolicyWrappedView {
    /// Check if this is a root/unrestricted policy
    pub fn is_root(&self) -> bool;

    /// Get the underlying policy context
    pub fn policy(&self) -> &PolicyContext;

    /// Get the policy enforcer for query execution
    pub fn enforcer(&self) -> &Arc<QueryPolicyEnforcer>;
}
}

Best Practices

1. Prefer Identity-Based Policies

Store policies in the database for:

  • Version control with data
  • Audit trail of policy changes
  • Dynamic policy updates without code changes
  • Time-travel to historical policy states

2. Use HTTP IRIs for Identities

HTTP IRIs are more portable than DIDs for identity subjects:

#![allow(unused)]
fn main() {
// Recommended
let identity = "http://example.org/identity/alice";

// Also works but may have encoding issues
let identity = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
}

3. Always Set default_allow Explicitly

#![allow(unused)]
fn main() {
// Be explicit about default behavior
let wrapped = wrap_identity_policy_view(&ledger, identity, false).await?;
//                                                          ^^^^^ explicit deny
}

4. Handle Policy Errors

#![allow(unused)]
fn main() {
let graph = GraphDb::from_ledger_state(&ledger)
    .with_policy(std::sync::Arc::new(policy_ctx));

match graph.query(&fluree).jsonld(&query).execute().await {
    Ok(result) => process_results(result),
    Err(ApiError::PolicyDenied { message, policy_id }) => {
        log::warn!("Access denied by {}: {}", policy_id, message);
        // Return empty or error to user
    }
    Err(e) => return Err(e),
}
}