Repository navigation
Document Authorization
Module Reference: oc-core (DocumentAccessGate, AuthorizationProvider, EffectiveAcl), oc-authorization-openfga, oc-mcp-server
Every document OpenCrawling retrieves for an AI agent, Large Language Model (LLM), or API client through the Model Context Protocol (MCP) tools is authoritatively evaluated against that document's access permissions on the server, before any text, metadata, or chunk enters the model's context window.
OpenCrawling supports pluggable document authorization:
-
Metadata Stored ACLs (
metadatamode, default): Evaluates the Access Control Lists stored with each chunk in the vector store usingEffectiveAclandMcpAclEvaluator. - Relationship-Based Access Control (ReBAC) with OpenFGA: Decouples permission evaluation into a high-performance fine-grained authorization engine, enabling dynamic group membership changes, transitive role hierarchies, and immediate permission revocation without re-indexing chunks.
-
Safe Migration Modes (
shadowandintersection): Enables operators to validate OpenFGA against stored ACL decisions with zero downtime and zero false-positive access grants before cutting over.
flowchart TD
subgraph Client["AI Agent / LLM Client Boundary"]
Caller["Caller Identity<br/>(userPrincipal + userRoles)"]
end
subgraph Store["Search & Retrieval Engine Layer"]
Backends["Vector / Search Backends (9 Engines)<br/>pgvector / Vespa / Qdrant / Milvus / OpenSearch / Solr / Luxir / OpenArch"]
Candidates["Ranked Top-K Candidates<br/>(Candidate Window: limit × overfetch-factor)"]
end
subgraph GateLayer["Unified Enforcement Layer (Fail-Closed)"]
Gate["DocumentAccessGate<br/>(Single access gate for all stores)"]
SPI["AuthorizationProvider SPI<br/>(Batch evaluation per page)"]
end
subgraph Providers["Authorization Providers"]
Stored["StoredAclAuthorizationProvider<br/>(EffectiveAcl / McpAclEvaluator)"]
FGA["OpenFgaAuthorizationProvider<br/>(OpenFGA BatchCheck API v1.22.0)"]
end
Caller --> Backends
Backends --> Candidates
Candidates --> Gate
Gate --> SPI
SPI -.->|metadata mode| Stored
SPI -.->|shadow mode| Stored & FGA
SPI -.->|intersection mode| Stored & FGA
SPI -.->|openfga mode| FGA
Gate -->|"Only Authorized Documents (Fail-Closed)"| Caller
-
One Access Gate for Every Store: All 9 MCP search backends (PostgreSQL/pgvector, Qdrant, Milvus, OpenSearch 2.x, OpenSearch 3.x, Apache Solr, Vespa, Luxir, and Hyland OpenArch) hand their search hits directly to
DocumentAccessGate. No store backend makes its own access decision. -
Batch Evaluation: The gate queries the configured
AuthorizationProviderfor the whole candidate page in a single batch call (canRead(Caller, List<ResourceRef>)), minimizing network round-trips. - Fail-Closed Semantics: A document with missing or unreadable permission data is immediately denied. If the authorization provider encounters an error (engine outage, network timeout, malformed payload), the entire request fails closed. OpenCrawling never falls back to weaker security rules.
Connectors harvest document security from heterogeneous repositories as a structured SecurityConfig (a collection of PermissionRule entries defining identity, identityType, and access), optionally accompanied by legacy comma-separated acl strings.
The unified normalizer EffectiveAcl translates these rules into consistent fields indexed across every search engine (security_allowed_read, security_denied_read, security_inheritance) and mirrored into OpenFGA:
| Source Permission Rule | Normalized Effect |
|---|---|
access = "read", "write", or "admin"
|
Grants read access to the specified identity. |
access = "deny" |
Explicitly denies read access. An explicit deny strictly supersedes any allow grant. |
identityType = "public" (e.g. CMIS anonymous, GROUP_EVERYONE) |
Indexed as public: readable by all callers. |
| Structured rules present | Structured rules authoritatively decide; legacy flat acl strings are ignored. |
Default public placeholder rule + legacy acl naming identities |
The legacy acl identities take precedence: an unauthenticated connector cannot accidentally make restricted documents public. |
No structured rules, only legacy acl
|
Tokens in acl grant read access ("read" itself is filtered as a reserved keyword). |
Neither structured rules nor legacy acl
|
Nobody may read the document (fail-closed default). |
- Case-insensitive comparison across all identities, user principals, and roles.
- Leading
user:orgroup:prefixes are stripped symmetrically during comparison (e.g., a grant togroup:financematches caller rolefinanceorgroup:finance). - Identities containing commas (such as LDAP Distinguished Names:
CN=Alice,OU=Finance,DC=corp) are parsed and matched as intact whole strings without improper delimiter splitting.
Note
Today the caller's principal and roles are asserted by the MCP client in tool arguments (userPrincipal, userRoles). Token-based verification (OAuth2/OIDC RBAC) is governed by issue #187; the Caller abstraction already incorporates an authenticated flag ready for token propagation.
Configured via opencrawling.security.authorization.mode (OPENCRAWLING_SECURITY_AUTHORIZATION_MODE):
| Mode | Authoritative Decision Maker | Recommended Phase | Behavior & Rollout Context |
|---|---|---|---|
metadata (default)
|
Permissions stored with each chunk in the vector store | Production (standalone) | Default mode. OpenFGA services are not started. Uses McpAclEvaluator and stored vector payloads. |
shadow |
Stored vector permissions decide; OpenFGA evaluated asynchronously | Pre-migration validation | Stored ACLs govern the response returned to the caller. OpenFGA is queried concurrently; disagreements increment oc.authz.shadow.mismatch and log warnings. |
intersection |
Both stored ACLs and OpenFGA must allow | Cautious cut-over | Dual-gate security: OpenFGA can only restrict access further, never inadvertently expand it. |
openfga |
OpenFGA alone decides | Target ReBAC state | True relationship-based access control. User group memberships and permission updates apply immediately without re-indexing vector stores. |
Important
Selecting shadow, intersection, or openfga requires the oc-authorization-openfga module. If required configuration properties are missing, the runtime fails startup immediately with an actionable error rather than silently falling back to metadata mode.
OpenCrawling bundles a fine-grained authorization schema (oc-authorization-openfga/src/main/resources/openfga/model.fga):
model
schema 1.1
type user
type group
relations
define member: [user, user:*, group#member]
type document
relations
define grantee: [user, user:*, group#member]
define blocked: [user, user:*, group#member]
define can_read: grantee but not blocked
-
Document Identifiers: Encoded as
document:<sha256-hex(document_id)>. This produces fixed-length 64-character tokens safe against arbitrary characters, URIs, and path separators. -
Identity Tuples:
- Each permission grant creates a
granteerelation tuple on the document. - Each explicit deny rule creates a
blockedrelation tuple on the document. - Public access maps to
user:*as agrantee. - Because stored-ACL matching supports both literal and prefix-stripped tokens, each identity generates two group representations:
r_<raw_identity>ands_<stripped_identity>. Identifiers are lowercased and percent-encoded.
- Each permission grant creates a
-
Contextual Evaluation:
- At query time, the caller's asserted
userPrincipalanduserRolesare supplied as contextual tuples to OpenFGA'sBatchCheckAPI. - Group memberships stored directly in OpenFGA (e.g.,
group:s_finance#member@user:p_dave) apply transitively, enabling enterprise LDAP/IdP synchronization.
- At query time, the caller's asserted
The AuthorizationSyncConsumer (oc-authorization-openfga) runs in the decoupled stack listening to the primary documents Kafka topic (opencrawling-documents) under consumer group opencrawling-authz-sync.
sequenceDiagram
autonumber
participant K as Kafka (opencrawling-documents)
participant C as AuthorizationSyncConsumer
participant F as OpenFGA Engine
participant DLT as Kafka (opencrawling-documents.authz-dlt)
K->>C: Document Ingestion / Update / Delete Message
C->>F: Read current tuples for document:<hash>
C->>C: Compute diff (additions & deletions)
alt Diff > 0
C->>F: Apply tuple diff (add blocked -> add grantee -> del grantee -> del blocked)
end
alt OpenFGA Error / Timeout
C->>C: Retry with Exponential Backoff (6 attempts, 0.5s -> 10s)
opt Retries Exhausted
C->>DLT: Publish message to dead-letter topic
end
end
-
Diff Calculation & Ordering: Syncs compute minimal tuple diffs. When changes exceed 100 tuples, mutations are strictly ordered:
add blocked→add grantee→remove grantee→remove blocked. This ensures a document is never temporarily more permissive during write windows. -
Tombstones & Deletions: OIS
DELETEmessages delete all relationship tuples for the target document, ensuring purged files immediately vanish from search results even if downstream store purges lag. - Retry with Exponential Backoff: Transient errors retry up to 6 times (backing off from 500ms to 10s).
-
Dead-Letter Topic (
opencrawling-documents.authz-dlt): Exhausted failures are isolated to the DLT without blocking the partition.
Warning
Do not replay the dead-letter topic directly. Replaying older failure messages verbatim could overwrite newer permission updates. To recover, resolve the OpenFGA outage and reset the consumer group offset to the original failed record offset (preserved in record header kafka_dlt-original-offset) or trigger a targeted re-crawl.
When operating in intersection or openfga modes, OpenFGA may deny documents that the vector store ranked highly. To prevent returned result pages from truncating or starving, search backends dynamically widen their candidate retrieval window:
-
overfetch-factor: Default10(e.g. requesting 5 results fetches 50 candidate hits). -
candidate-budget: Default500maximum candidates per query. -
Pre-Filter Bypass: In
intersectionandopenfgamodes, backends disable engine-level ACL pre-filters (e.g. PostgreSQLsecurity_allowed_readSQL clauses) so that candidates can be evaluated by OpenFGA.
| Property | Environment Variable | Default | Description |
|---|---|---|---|
opencrawling.security.authorization.mode |
OPENCRAWLING_SECURITY_AUTHORIZATION_MODE |
metadata |
Mode: metadata, shadow, intersection, or openfga. |
opencrawling.security.authorization.overfetch-factor |
OPENCRAWLING_SECURITY_AUTHORIZATION_OVERFETCH_FACTOR |
10 |
Candidate multiplication factor for ReBAC queries. |
opencrawling.security.authorization.candidate-budget |
OPENCRAWLING_SECURITY_AUTHORIZATION_CANDIDATE_BUDGET |
500 |
Hard cap on candidate chunks passed to the access gate. |
opencrawling.security.authorization.openfga.api-url |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_API_URL |
http://localhost:8080 |
OpenFGA HTTP endpoint. |
opencrawling.security.authorization.openfga.api-token |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_API_TOKEN |
(none) | Pre-shared key or Bearer token for OpenFGA authentication. |
opencrawling.security.authorization.openfga.store-id |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_STORE_ID |
(none) | Pinned OpenFGA Store ID. Pin in production. |
opencrawling.security.authorization.openfga.model-id |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_MODEL_ID |
(none) | Pinned OpenFGA Authorization Model ID. Pin in production. |
opencrawling.security.authorization.openfga.auto-provision |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_AUTO_PROVISION |
false |
Automatically creates store and model if unpinned (Dev/Testing). |
opencrawling.security.authorization.openfga.store-name |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_STORE_NAME |
opencrawling |
Store name used during auto-provisioning. |
opencrawling.security.authorization.openfga.timeout |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_TIMEOUT |
5s |
Timeout duration for OpenFGA API calls. |
opencrawling.security.authorization.openfga.batch-size |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_BATCH_SIZE |
50 |
Maximum check operations bundled per BatchCheck request. |
opencrawling.security.authorization.openfga.max-parallel-requests |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_MAX_PARALLEL_REQUESTS |
10 |
Concurrent BatchCheck HTTP calls allowed. |
opencrawling.security.authorization.openfga.sync.enabled |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_SYNC_ENABLED |
true |
Enables AuthorizationSyncConsumer in the runtime container. |
opencrawling.security.authorization.openfga.sync-group-id |
OPENCRAWLING_SECURITY_AUTHORIZATION_OPENFGA_SYNC_GROUP_ID |
opencrawling-authz-sync |
Consumer group ID for the authorization sync worker. |
Follow this phased zero-downtime migration path:
[Phase 1: Deploy OpenFGA] ──► [Phase 2: Shadow Mode] ──► [Phase 3: Backfill] ──► [Phase 4: Cutover]
docker-compose-openfga mode=shadow Kafka replay + recrawl mode=intersection / openfga
Deploy OpenFGA v1.22.0 with PostgreSQL storage overlay using docker-compose-openfga.yml:
export OPENFGA_PRESHARED_KEY="$(openssl rand -hex 32)"
docker compose -f docker-compose.yml -f docker-compose-openfga.yml up -d openfga postgres-openfgaSet OPENCRAWLING_SECURITY_AUTHORIZATION_MODE=shadow on oc-mcp-server. The sync consumer begins populating OpenFGA in real-time from active crawler jobs while stored ACLs remain authoritative.
-
Replay Kafka Topic: Reset the
opencrawling-authz-syncgroup to the beginning of the retainedopencrawling-documentstopic:kafka-consumer-groups.sh --bootstrap-server kafka:9092 --group opencrawling-authz-sync \ --topic opencrawling-documents --reset-offsets --to-earliest --execute
- Re-crawl Historical Repositories: Trigger crawls for documents older than the Kafka retention window.
Monitor Micrometer metric oc.authz.shadow.mismatch. Once mismatches stay at zero across normal production traffic, switch to intersection (conservative verification) or openfga (full ReBAC agility).
OpenCrawling exposes dedicated Micrometer authorization metrics under /actuator/metrics, tagged with provider:
| Metric Name | Type | Description |
|---|---|---|
oc.authz.check |
Timer | Latency distribution of candidate page authorization decisions. |
oc.authz.candidates |
Counter | Total candidate chunks evaluated by DocumentAccessGate. |
oc.authz.denied |
Counter | Total candidate chunks rejected due to insufficient permissions. |
oc.authz.errors |
Counter | Requests rejected due to provider timeouts or infrastructure failures. |
oc.authz.shadow.mismatch |
Counter | Decisions where OpenFGA disagreed with stored vector ACLs. |
oc.authz.shadow.errors |
Counter | OpenFGA errors during shadow evaluation (does not impact caller). |
-
Unit Tests:
-
EffectiveAcl: Validates 20,000+ permutations ensuring stores holding full security objects and stores holding flattened fields make identical decisions. -
OpenFgaIdentifiersTest: Verifies deterministic hashing, percent-encoding, and prefix stripping. -
TuplePlanTest: Validates fail-closed tuple diff ordering.
-
-
Integration Tests (
-DskipITs=false):-
OpenFgaAuthorizationIT: Testcontainers test exercising sync, live check, deletes, and fail-closed behavior during outages. -
OpenFgaDifferentialIT: Validates 37,500 random document and caller combinations comparing stored ACL decisions against real OpenFGA server evaluations.
-
-
End-to-End Decoupled Script:
Executes an 8-stage automated decoupled pipeline verification testing OIS document ingestion, per-caller search isolation, dynamic OpenFGA group membership additions/removals without re-indexing, tombstone deletions, fail-closed behavior under service shutdown, and metric emissions.
./scripts/test-openfga-decoupled.sh
- Home
- User & Installation Guide
- Quick Start
- VM & Bare-Metal Distribution Installation
- Local Development & Fast Docker Builds
- User Guide
- Architecture
- Zero-Trust Security & ACL Management
- Document Authorization & OpenFGA
- Document Lifecycle Actions & Tombstones
- Apache Tika 4 Text Extraction
- Configuration Guide
- Connectors Reference
- Alfresco Content Services (ACS) Connector
- Alfresco Process Services (APS) Connector
- Flowable Repository Connector
- Camunda Repository Connector
- OASIS CMIS Repository Connector
- Doxis Repository Connector
- M-Files Repository Connector
- SharePoint Online Repository Connector
- IBM FileNet P8 Repository Connector
- OpenText Documentum Repository Connector
- Hyland OpenArch Integration (Repository, Output & MCP)
- Relational Database (JDBC) Connector
- Apache StormCrawler Repository Connector
- gRPC Push Repository Connector
- Apache Solr Output Connector
- Luxir Output Connector
- Apache SeaTunnel Output Connector
- Apache Ozone Output Connector (Migration Mode)
- AWS S3 Output Connector (Migration Mode)
- Alfresco Content Services Output Connector (Migration Mode)
- Connector Execution Modes
- Model Context Protocol
- Apache Ozone Claim Check Store
- Lakehouse Audit Reporting & Ledger
- AI-Powered Observability (AIOps)
- Auto-Narrativization Copilot
- Maven Connector Archetypes
- Java Client SDK
- OpenCrawling CLI
- OpenAPI Specification & Swagger UI
- gRPC Internal Transport
- Migration Guide (ManifoldCF)
- Integration Test — Decoupled Pipeline
- M-Files Integration Test
- SharePoint Online Integration Test
- IBM FileNet Integration Test
- OpenText Documentum Integration Test
- Hyland OpenArch Integration Test
- Local Docker Build Test
- Root CLI Runner Scripts Test
- Secure MCP Server Integration Test
- Narrativization Integration Test
- Reporting Integration Test
- AWS S3 Migration Integration Test
- Alfresco Migration Integration Test
- OpenFGA Authorization Integration Test