Skip to content

Access control ​

Moʻolelo's authorization layer is designed around a single invariant: the platform never grants access the source system would not grant. When a user queries Moʻolelo, the answer they get back is exactly the subset of relevant records that they are allowed to read in the upstream sources — no more, no less.

This page explains how the platform makes that mechanically true.

The big picture ​

text
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│  Content source  │    │  Identity source │    │ Permission grant │
│  (Confluence,    │    │  (Entra ID,      │    │  (per-page       │
│   SharePoint,    │    │   Atlassian,     │    │   restriction,   │
│   GitHub)        │    │   GitHub orgs)   │    │   site role,     │
│                  │    │                  │    │   collaborator)  │
└────────┬─────────┘    └────────┬─────────┘    └────────┬─────────┘
         │                       │                       │
         ▼                       ▼                       ▼
   ┌─────────────────────────────────────────────────────────────┐
   │                       acl-projector                         │
   │  Translates source-system ACLs + identity memberships into  │
   │  OpenFGA tuples in the tenant's authorization store.        │
   └─────────────────────────────┬───────────────────────────────┘
                                 ▼
                    ┌────────────────────────┐
                    │   OpenFGA tenant store │
                    │  ─────────────────────  │
                    │  user:alice@…          │
                    │   ↳ viewer ↳ entity:…  │
                    │   ↳ member ↳ group:…   │
                    └─────────────┬──────────┘
                                  ▼
                    ┌────────────────────────┐
                    │   query-coordinator    │
                    │  Filters retrieval     │
                    │  candidates against    │
                    │  the user's identity.  │
                    └────────────────────────┘

Three streams converge in the projector:

  1. Content-side ACLs from each connector. When the connector indexes a Confluence page, it also fetches that page's read restrictions; when it indexes a GitHub repo, it fetches the collaborators and team grants. Each grant becomes one ACL change event.
  2. Identity catalog updates from the identity-collector. User upserts populate the alias store so non-email principals (a GitHub login like alice) resolve to the canonical email (alice@example.com). Group membership updates become OpenFGA group#member tuples.
  3. Source authority chain kept on every ACL grant. When the projector writes a tuple it records why — which source policy justified the grant — so the audit warehouse can reproduce the reasoning later.

What gets written to OpenFGA ​

The authorization model has four types:

TypeWhat it represents
userA canonical-person identity (user:alice@example.com).
groupA source-system group (group:engineers, group:acme/backend).
entityA record-level ACL anchor (entity:confluence:page:123).
chunkThe searchable unit; inherits from parent_entity.

Direct grants land on entity:; the chunks the search index returns inherit access from their parent entity. This keeps the OpenFGA tuple count proportional to the number of records, not the number of chunks (which is typically 5–50× larger).

For deployments that need finer-grained authorization (clearance levels, classification labels), the model includes a conditional cleared_at_or_above grant on the chunk type:

text
type chunk
  relations
    define parent_entity: [entity]
    define viewer: [user, group#member] or
                   viewer from parent_entity or
                   user with cleared_at_or_above

The cleared_at_or_above condition compares the user's clearance level (passed in the request context at query time) against the document's classification level (stored on the tuple). The two fields are operator-configured at deployment time.

Per-source ACL mapping ​

SourceWhat's harvestedOpenFGA tuple shape
GitHubRepo collaborators + team permissionsuser:<email> ↔ viewer ↔ entity:github:repo:<owner>/<name>
ConfluencePage-level read restrictions (when explicit)user:<email> ↔ viewer ↔ entity:confluence:page:<id>
SharePointDrive-item permissions + sensitivity labelsuser:<email> ↔ viewer ↔ entity:sharepoint:driveitem:<id> (+ classification condition)

For Confluence, pages with no explicit restriction inherit from their space — the projector emits no event in that case, and the inheritance shows up via the parent_entity relation written by the chunk pipeline. Same shape for SharePoint items in folders without explicit grants.

Reconciliation ​

OpenFGA tuple writes are idempotent on (user, relation, object), so a Pulsar redelivery after a sink restart cannot create a duplicate tuple. The reconciliation strategy is:

  • Continuous — every ACL change event the connector emits and every membership change the identity-collector emits flows through within seconds.
  • On-restart replay — the projector resumes from the persisted Pulsar offset; idempotency on tuple keys guarantees no double-grant.
  • Periodic full re-snapshot — on the roadmap is a scheduled (default weekly) re-snapshot pass that compares projector-side state against OpenFGA's view and fixes drift from any missed deliveries.

Unknown principals ​

Some source-system ACLs reference principals the identity catalog doesn't know about — typically external collaborators or a GitHub user whose email isn't published. When that happens, the projector:

  1. Emits an acl.unknown_principal audit event with the source reference, the resource, and the ACL grant the projector couldn't resolve.
  2. Skips the tuple — the principal doesn't get access through Moʻolelo, even if they have it in the source.

Operators inspect these events in the audit warehouse and resolve them by either:

  • Adding an explicit (provider, provider_id) → canonical_email alias via the admin API.
  • Re-running the identity-collector for the relevant source.
  • Accepting the principal as out-of-scope — typical for external collaborators on a GitHub repo.

The "fail closed" default is deliberate: granting access to a principal whose identity Moʻolelo can't verify would violate the "never more than the source system grants" invariant.

Per-tenant isolation ​

Each tenant has its own OpenFGA store (store_id = tenant_<id>). The provisioner creates the store at tenant-create time, ships the authorization model, and persists (store_id, model_id, endpoint) on tenant_acl_config. The acl-projector and the query-coordinator both read this row at boot to know which store to write to / check against.

There is no cross-tenant tuple traffic. A bug in one tenant's projector cannot leak into another tenant's authorization store.

Where ACLs get enforced ​

Moʻolelo enforces access in three complementary places:

  • At projection — the acl-projector writes OpenFGA tuples that reflect each source's permissions, and emits an acl.unknown_principal event whenever it can't resolve someone.
  • At the query coordinator — the coordinator runs the OpenFGA evaluator over candidate results, applies per-source policy hooks for permissions OpenFGA can't model directly, and emits an audit row per served result with the authority chain.
  • Inside Vespa — an Apache 2.0 Searcher plugin filters retrieval candidates inside Vespa via OpenFGA BatchCheck, ListObjects, or static-attribute pre-filter. Per-query latency stays bounded regardless of corpus size, and ReBAC-shaped permissions (group-of-group, transitive sharing, parent-folder-implies-child-access) are honored at the retrieval layer with no recall loss. The plugin lives in a clearly isolated subdirectory so it can be extracted into its own project later. See Vespa OpenFGA plugin for the operator-facing setup.

On the roadmap:

  • Periodic full re-snapshot reconciliation (in addition to the continuous projection that ships today).
  • ML-driven identity resolution for users whose emails aren't exposed by their source systems.
  • ReBAC-graph-aware ACL projection for tenants with deep permission inheritance trees beyond what parent_entity covers.

Moʻolelo — self-hosted federated enterprise knowledge platform.