Permissions & Access Control¶
Complete guide to Nexus permissions using ReBAC (Relationship-Based Access Control)
Table of Contents¶
- Overview
- Quick Start
- Core Concepts
- Permission Model
- API Patterns
- Namespace Design
- Multi-Tenant Isolation
- Remote Server Usage
- Common Patterns
- Best Practices
- Troubleshooting
Overview¶
Nexus uses a pure ReBAC (Relationship-Based Access Control) permission system based on Google's Zanzibar design. This provides fine-grained, flexible, and scalable authorization for multi-tenant AI file systems.
Key Features¶
- ✅ Pure ReBAC: All permissions defined as relationships between subjects and objects
- ✅ Subject-Based: Identity specified per-operation, not per-instance
- ✅ Multi-Tenant: Complete isolation between tenants
- ✅ Flexible: Supports users, agents, groups, services, and custom entity types
- ✅ Scalable: Graph-based permission checking with caching
- ✅ Standard: Industry-proven Zanzibar model
All permissions are explicitly managed through ReBAC relationships.
Quick Start¶
Core Concepts¶
Core Concepts¶
Subject (Who)¶
The entity requesting access: - Type: user, agent, service, group, etc. - ID: Unique identifier
Object (What)¶
The resource being accessed: - Type: file, memory, workspace, etc. - ID: Path or unique identifier
object = ("file", "/workspace/doc.txt")
object = ("memory", "mem_12345")
object = ("workspace", "ws_sales")
Relation (How)¶
The relationship type between subject and object: - Direct Relations: direct_owner, direct_editor, direct_viewer - Computed Relations: owner, editor, viewer (unions of direct + indirect) - Permissions: read, write, execute (maps to relations via namespace config)
relation = "direct_editor" # Alice is a direct editor
relation = "editor" # Includes direct + inherited via groups
permission = "write" # Maps to "editor" or "owner" via namespace
Permission Hierarchy¶
owner (full access)
└── write (includes read)
└── read (view only)
Relations:
- owner = direct_owner ∪ parent_owner
- editor = direct_editor ∪ owner
- viewer = direct_viewer ∪ editor
Permission Mapping¶
| Permission | ReBAC Relations | Capabilities |
|---|---|---|
READ | viewer, editor, owner | Read file content, list directories |
WRITE | editor, owner | Create, update, delete files |
EXECUTE | owner | Execute files (currently owner-only) |
ReBAC (Relationship-Based Access Control)¶
Zanzibar Model¶
Nexus implements Google Zanzibar's authorization model:
- Tuples: (subject, relation, object) relationships
- Namespaces: Permission expansion rules per object type
- Check API: Fast permission verification with graph traversal
- Expand API: Find all subjects with a permission
Namespace Configuration¶
Defines how permissions expand for an object type:
DEFAULT_FILE_NAMESPACE = {
"object_type": "file",
"relations": {
# Structural relation: parent directory
"parent": {},
# Direct relations (granted explicitly)
"direct_owner": {},
"direct_editor": {},
"direct_viewer": {},
# Parent inheritance via tupleToUserset
"parent_owner": {
"tupleToUserset": {
"tupleset": "parent",
"computedUserset": "owner"
}
},
"parent_editor": {
"tupleToUserset": {
"tupleset": "parent",
"computedUserset": "editor"
}
},
"parent_viewer": {
"tupleToUserset": {
"tupleset": "parent",
"computedUserset": "viewer"
}
},
# Computed relations (union of direct + parent inheritance)
"owner": {"union": ["direct_owner", "parent_owner"]},
"editor": {"union": ["direct_editor", "parent_editor", "owner"]},
"viewer": {"union": ["direct_viewer", "parent_viewer"]},
},
# Explicit permission-to-userset mapping (Zanzibar-style)
# Prevents ambiguous check("write") bugs by defining exact semantics
"permissions": {
"read": ["viewer", "editor", "owner"], # Read = viewer OR editor OR owner
"write": ["editor", "owner"], # Write = editor OR owner (NOT viewer)
"execute": ["owner"], # Execute = owner only
}
}
Key Insight: The permissions mapping explicitly defines which relations grant which permissions. This eliminates ambiguity and follows Google Zanzibar's userset semantics.
Relationship Types¶
1. Direct Relationships¶
Explicitly created tuples:
# Alice owns /workspace/doc.txt
nx.rebac_create(
subject=("user", "alice"),
relation="direct_owner",
object=("file", "/workspace/doc.txt"),
)
2. Union Relationships¶
Logical OR of multiple relations:
# "editor" = "owner" OR "direct_editor"
# If alice is owner OR direct_editor, she has editor permission
3. Indirect Relationships (tupleToUserset)¶
Permission via another object:
# "parent_owner" = "if you're owner of the parent, you're owner of this"
# If alice owns /workspace, she owns /workspace/doc.txt
Graph Traversal¶
Permission checking traverses the relationship graph:
Check: Can alice WRITE to /workspace/sales/doc.txt?
1. Direct check: alice --write--> /workspace/sales/doc.txt? NO
2. Union expansion: write = editor ∪ owner
3. Check editor:
a. alice --direct_editor--> file? NO
b. alice --owner--> file? → expand owner
4. Check owner:
a. alice --direct_owner--> file? NO
b. alice --parent_owner--> file?
i. Find parent: file --parent--> /workspace/sales
ii. alice --owner--> /workspace/sales? YES ✓
5. Result: ALLOW
API Patterns¶
Basic Operations¶
Namespace Design¶
Real-World Examples¶
Example 1: GitHub-style Repository Permissions¶
repo_namespace = NamespaceConfig(
namespace_id="repo-001",
object_type="repository",
config={
"relations": {
# Direct relations
"direct_admin": {},
"direct_write": {},
"direct_read": {},
"direct_triage": {},
# Admin (full control)
"admin": {"union": ["direct_admin"]},
# Write (push access + admin)
"write": {"union": ["direct_write", "admin"]},
# Triage (manage issues + write)
"triage": {"union": ["direct_triage", "write"]},
# Read (view + all above)
"read": {"union": ["direct_read", "triage"]}
},
"permissions": {
"view": ["read"],
"comment": ["read"],
"create_issue": ["read"],
"push": ["write"],
"manage_issues": ["triage"],
"manage_settings": ["admin"],
"delete": ["admin"]
}
}
)
Example 2: Google Drive-style Sharing¶
drive_namespace = NamespaceConfig(
namespace_id="drive-001",
object_type="drive-file",
config={
"relations": {
# Structural
"parent": {},
# Direct
"direct_owner": {},
"direct_editor": {},
"direct_commenter": {},
"direct_viewer": {},
# Inherited from parent
"parent_owner": {
"tupleToUserset": {
"tupleset": "parent",
"computedUserset": "owner"
}
},
"parent_editor": {
"tupleToUserset": {
"tupleset": "parent",
"computedUserset": "editor"
}
},
"parent_viewer": {
"tupleToUserset": {
"tupleset": "parent",
"computedUserset": "viewer"
}
},
# Computed
"owner": {"union": ["direct_owner", "parent_owner"]},
"editor": {"union": ["direct_editor", "parent_editor", "owner"]},
"commenter": {"union": ["direct_commenter", "editor"]},
"viewer": {"union": ["direct_viewer", "parent_viewer", "commenter"]}
},
"permissions": {
"view": ["viewer"],
"comment": ["commenter"],
"edit": ["editor"],
"share": ["owner"],
"delete": ["owner"]
}
}
)
Example 3: Slack-style Channel Permissions¶
channel_namespace = NamespaceConfig(
namespace_id="channel-001",
object_type="channel",
config={
"relations": {
"workspace_member": {}, # Member of workspace
"channel_member": {}, # Explicitly in channel
"channel_admin": {}, # Channel admin
"workspace_admin": {}, # Workspace admin
# Admins (channel or workspace)
"admin": {"union": ["channel_admin", "workspace_admin"]},
# Members (must be in channel AND workspace)
"member": {
"intersection": ["channel_member", "workspace_member"]
},
# Can post (members or admins)
"poster": {"union": ["member", "admin"]}
},
"permissions": {
"read": ["member"],
"post": ["poster"],
"manage": ["admin"],
"archive": ["admin"]
}
}
)
Permission Lattice Pattern¶
A common pattern is to create a permission lattice where higher privileges include lower ones:
Implementation:
"viewer": {"union": ["direct_viewer"]},
"editor": {"union": ["direct_editor", "viewer"]}, # editor includes viewer
"owner": {"union": ["direct_owner", "editor"]} # owner includes editor
Result: - Owner can do everything (owner + editor + viewer) - Editor can edit and view (editor + viewer) - Viewer can only view (viewer)
Multi-Tenant Isolation¶
Tenant ID¶
Every operation includes a tenant_id for data isolation:
# Alice in org_acme
nx.read("/workspace/doc.txt", subject=("user", "alice"), tenant_id="org_acme")
# Bob in org_techcorp (different tenant)
nx.read("/workspace/doc.txt", subject=("user", "bob"), tenant_id="org_techcorp")
Isolation Mechanisms¶
1. Database-Level¶
Metadata filtered by tenant_id:
2. Permission-Level (ReBAC)¶
ReBAC tuples include tenant_id for isolation:
# Tenant ID validation at write-time
# Create permission tuple with tenant isolation
nx.rebac_create(
subject=("user", "alice"),
relation="direct_editor",
object=("file", "/workspace/doc.txt"),
# Tenant ID is now stored in the tuple
)
# Cross-tenant relationships are REJECTED at write-time
try:
# This will FAIL - alice from org_acme cannot access org_techcorp files
nx.rebac_create(
subject=("user", "alice"), # org_acme user
relation="viewer",
object=("file", "/workspace/doc.txt"), # org_techcorp file
# Cross-tenant tuple rejected!
)
except ValueError as e:
print(f"❌ {e}") # "Cross-tenant relationship not allowed"
Security: Write-time validation prevents cross-tenant permission leaks (P0 fix).
3. Storage-Level¶
Physical storage organized by tenant:
/data/
├── org_acme/
│ └── workspace/
│ └── doc.txt
└── org_techcorp/
└── workspace/
└── doc.txt # Different file!
Cross-Tenant Access¶
By default, cross-tenant access is prohibited:
# Alice (org_acme) tries to access org_techcorp file
try:
nx.read(
"/workspace/doc.txt",
subject=("user", "alice"),
tenant_id="org_techcorp" # Different tenant!
)
except PermissionError:
print("❌ Cross-tenant access denied")
Remote Server Usage¶
Common Patterns¶
Pattern 1: Organization Setup¶
# 1. Create organization structure
org_id = "org_acme"
nx.mkdir(f"/orgs/{org_id}", subject=("service", "setup"), is_admin=True)
nx.mkdir(f"/orgs/{org_id}/workspaces", subject=("service", "setup"), is_admin=True)
# 2. Create workspace
workspace_id = "sales"
workspace_path = f"/orgs/{org_id}/workspaces/{workspace_id}"
nx.mkdir(workspace_path, subject=("service", "setup"), is_admin=True)
# 3. Grant admin access
nx.rebac_create(
subject=("user", "org_admin"),
relation="direct_owner",
object=("file", workspace_path),
)
# 4. Grant team access
nx.rebac_create(
subject=("group", "sales_team"),
relation="direct_editor",
object=("file", workspace_path),
)
Pattern 2: Workspace Sharing¶
# Share workspace with another user
def share_workspace(nx, workspace_path, user_id, role="viewer"):
"""Share workspace with user (viewer or editor)."""
# Grant all files in workspace
for file_path in nx.glob(f"{workspace_path}/**/*"):
nx.rebac_create(
subject=("user", user_id),
relation=f"direct_{role}", # direct_viewer or direct_editor
object=("file", file_path),
)
print(f"✅ Shared {workspace_path} with {user_id} as {role}")
# Usage
share_workspace(nx, "/orgs/acme/workspaces/sales", "bob", role="editor")
Pattern 3: Hierarchical Permissions¶
# Parent-child permission inheritance (requires parent tuples)
def setup_directory_permissions(nx, parent_dir, child_file):
"""Set up parent-child relationship for permission inheritance."""
# Create parent relationship tuple
nx.rebac_create(
subject=("file", child_file),
relation="parent",
object=("file", parent_dir),
)
# Now if alice owns parent_dir, she automatically owns child_file
# (via parent_owner relation in namespace config)
Pattern 4: Role-Based Access¶
# Define roles with permission templates
ROLES = {
"owner": "direct_owner",
"admin": "direct_owner",
"editor": "direct_editor",
"viewer": "direct_viewer",
"guest": "direct_viewer",
}
def grant_role(nx, user_id, resource_path, role):
"""Grant role-based access to resource."""
relation = ROLES.get(role)
if not relation:
raise ValueError(f"Unknown role: {role}")
nx.rebac_create(
subject=("user", user_id),
relation=relation,
object=("file", resource_path),
)
Pattern 5: Audit Trail¶
# Track permission changes via changelog
from datetime import datetime, UTC
def audit_permissions(nx, object_path):
"""Get permission change history for a file."""
# Query rebac_changelog table
conn = nx.rebac_manager._get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT change_type, subject_type, subject_id, relation, created_at
FROM rebac_changelog
WHERE object_type = 'file' AND object_id = ?
ORDER BY created_at DESC
""", (object_path,))
changes = cursor.fetchall()
for change in changes:
print(f"{change['created_at']}: {change['change_type']} - "
f"{change['subject_type']}:{change['subject_id']} "
f"{change['relation']}")
Security Best Practices¶
1. Principle of Least Privilege¶
Grant minimum necessary permissions:
# ✅ Good: Read-only access
nx.rebac_create(
subject=("user", "viewer"),
relation="direct_viewer", # Not editor or owner
object=("file", "/data/report.pdf"),
)
# ❌ Bad: Unnecessary owner access
nx.rebac_create(
subject=("user", "viewer"),
relation="direct_owner", # Too much!
object=("file", "/data/report.pdf"),
)
2. Use Groups for Team Access¶
Avoid per-user grants for large teams:
# ✅ Good: Use groups
nx.rebac_create(("group", "sales_team"), "direct_editor", ("file", "/sales/data"))
nx.rebac_create(("user", "alice"), "member", ("group", "sales_team"))
nx.rebac_create(("user", "bob"), "member", ("group", "sales_team"))
# ❌ Bad: Individual grants (hard to manage at scale)
nx.rebac_create(("user", "alice"), "direct_editor", ("file", "/sales/data"))
nx.rebac_create(("user", "bob"), "direct_editor", ("file", "/sales/data"))
# ... 50 more users
3. Temporary Access with Expiry¶
Use expires_at for short-term access:
from datetime import datetime, timedelta, UTC
# Guest access for 24 hours
nx.rebac_create(
subject=("user", "guest"),
relation="direct_viewer",
object=("file", "/shared/demo.pdf"),
expires_at=datetime.now(UTC) + timedelta(hours=24),
)
4. Audit Permission Changes¶
Monitor the changelog:
# Regular audit of permission changes
conn = nx.rebac_manager._get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM rebac_changelog
WHERE created_at > NOW() - INTERVAL '1 day'
ORDER BY created_at DESC
""")
for change in cursor.fetchall():
print(f"⚠️ {change['change_type']}: {change['subject_id']} "
f"{change['relation']} {change['object_id']}")
5. Separate Admin Operations¶
Use dedicated admin subjects for setup:
# ✅ Good: Separate bootstrap subject
BOOTSTRAP_SUBJECT = ("service", "bootstrap")
nx.write("/system/config", data, subject=BOOTSTRAP_SUBJECT, is_admin=True)
# Then grant specific access
nx.rebac_create(("user", "admin"), "owner", ("file", "/system/config"))
# ❌ Bad: Using user as admin everywhere
nx.write("/file.txt", data, subject=("user", "alice"), is_admin=True)
6. Validate Subject Sources¶
Always verify subject authenticity:
# ✅ Good: Server extracts from authenticated token
auth_result = await auth.authenticate(api_key)
subject = (auth_result.subject_type, auth_result.subject_id)
# ❌ Bad: Trust client-provided subject without verification
subject = request.json["subject"] # NEVER DO THIS!
7. Tenant Isolation¶
Always specify tenant_id for multi-tenant operations:
# ✅ Good: Explicit tenant isolation
nx.read("/data/file.txt", subject=("user", "alice"), tenant_id="org_acme")
# ❌ Bad: Missing tenant_id (may leak across tenants)
nx.read("/data/file.txt", subject=("user", "alice"))
Problem: "PermissionError: Access denied"¶
Cause: Subject doesn't have required ReBAC relationship.
Solution:
# 1. Check who has access
subjects = nx.rebac_expand(permission="read", object=("file", "/path"))
print(f"Current access: {subjects}")
# 2. Grant access
nx.rebac_create(
subject=("user", "alice"),
relation="direct_viewer", # For read access
object=("file", "/path"),
)
# 3. Verify
has_access = nx.rebac_check(("user", "alice"), "read", ("file", "/path"))
print(f"Alice can now read: {has_access}")
Problem: "Directory permissions don't work"¶
Cause: Directory-level permissions with child inheritance not yet implemented.
Solution: Grant per-file or use helper:
def grant_directory_access(nx, user, directory, role):
for path in nx.glob(f"{directory}/**/*"):
nx.rebac_create(
subject=("user", user),
relation=f"direct_{role}",
object=("file", path),
)
grant_directory_access(nx, "alice", "/workspace/sales", "editor")
Problem: "Group permissions not working"¶
Cause: User not added to group, or namespace doesn't support groups.
Solution:
# 1. Add user to group
nx.rebac_create(
subject=("user", "alice"),
relation="member",
object=("group", "engineering"),
)
# 2. Grant group access
nx.rebac_create(
subject=("group", "engineering"),
relation="direct_editor",
object=("file", "/projects/feature.md"),
)
# 3. Verify
has_access = nx.rebac_check(("user", "alice"), "write", ("file", "/projects/feature.md"))
Problem: "Permission check is slow"¶
Cause: Deep graph traversal without caching.
Solution:
# 1. Check cache TTL
rebac_manager = nx.rebac_manager
print(f"Cache TTL: {rebac_manager.cache_ttl_seconds}s")
# 2. Increase cache TTL (default: 5 minutes)
rebac_manager.cache_ttl_seconds = 600 # 10 minutes
# 3. Clean up expired cache
rebac_manager.cleanup_expired_cache()
# 4. Simplify relationship graph (avoid deep nesting)
Problem: "Cross-tenant access leak"¶
Cause: Missing or incorrect tenant_id in operations.
Solution:
# ✅ Always specify tenant_id
nx.read("/file.txt", subject=("user", "alice"), tenant_id="org_acme")
# Verify isolation
alice_acme_access = nx.rebac_check(
("user", "alice"), "read", ("file", "/file.txt")
) # tenant_id=org_acme
alice_techcorp_access = nx.rebac_check(
("user", "alice"), "read", ("file", "/file.txt")
) # tenant_id=org_techcorp (should be False)
Summary¶
Nexus uses pure ReBAC for all permissions: - ✅ Explicit relationships define access - ✅ Subject-based operations - ✅ Multi-tenant isolation - ✅ Flexible and scalable - ✅ Industry-standard Zanzibar model
See Also: - Authentication Guide - Multi-Tenant Guide - API Reference