Skip to content

Access & partitions

Multi-tenant and RBAC isolation is built on partitions. Python ships the composable pre-filter pieces — resolve a scope to a partition, hard-filter candidate partitions, apply an opaque ACL predicate — and you wire them into your own authorization flow. Go and JavaScript ship no access module, so the same three steps are ~20 lines you own; the shape is identical because the rule is just “contiguous prefix from the root, matched before retrieval”.

Python — from citenexus.access import resolve_scope, filter_partitions, allowed_partition, apply_acl_predicate:

  • resolve_scope(scope, hierarchy)PartitionPath — turn a scope dict (e.g. {"org": "acme", "team": "legal"}) into a contiguous-prefix partition. A gap or unknown key raises ValueError.
  • allowed_partition(candidate, allowed_set)bool — prefix match; an empty allowed set makes nothing visible.
  • filter_partitions(candidates, allowed_set)list[PartitionPath] — the hard, order-preserving pre-filter.
  • apply_acl_predicate(objects, acl_of, predicate=None) — the opaque second stage: your predicate decides; the library never interprets the ACL. None = keep all.

Go and JavaScript have no access package — the tabs below implement the same four functions over a partition string.

from citenexus.access import resolve_scope, filter_partitions, apply_acl_predicate
# 1. Resolve the caller's scope to the partition they're allowed to see.
allowed_path = resolve_scope({"org": "acme", "team": "legal"}, hierarchy=["org", "team"])
print(allowed_path.as_pairs()) # (('org', 'acme'), ('team', 'legal'))
# 2. Hard pre-filter your candidate partitions to that set BEFORE retrieval.
other = resolve_scope({"org": "globex", "team": "legal"}, hierarchy=["org", "team"])
visible = filter_partitions([allowed_path, other], {allowed_path})
print(len(visible)) # 1
# 3. Then the opaque ACL stage — your predicate decides, the library never looks inside.
objects = [{"id": "a", "acl": {"role": "legal"}}, {"id": "b", "acl": {"role": "hr"}}]
print(apply_acl_predicate(objects, lambda o: o["acl"], lambda acl: acl["role"] == "legal"))
# [{'id': 'a', 'acl': {'role': 'legal'}}]
# 4. Ingest tenant data under its partition, carrying an opaque acl.
rag.ingest("contracts/nda.pdf", acl={"role": "legal"}) # acl is carried, not enforced

A gap in the scope is refused rather than silently widened:

resolve_scope({"team": "legal"}, hierarchy=["org", "team"])
# ValueError: scope has a gap before level 'team': a partition prefix must be
# contiguous from the root (hierarchy=['org', 'team'])

The design keeps CiteNexus out of your identity system: it gives you a hard partition boundary and an ACL hook, and lets your host own who-can-see-what. Note what the tabs have in common — the filter runs before the ask, never after. Filtering an answer is not access control; the ungrantable passage must never reach the generator in the first place.