Authorization: Roles & Permissions
Access control for organizations, projects, and resources in Sibyl.
Overview
Sibyl uses a hierarchical authorization model:
Organization (org-level roles)
└── Projects (project-level roles)
└── Resources (entities, tasks, documents)Key Concepts:
- Organization Roles:
owner,admin,member,viewer- inherited across all projects - Project Roles:
project_owner,project_maintainer,project_contributor,project_viewer- scoped to specific projects - Org Isolation: graph memory is namespace-isolated per organization; content and auth resources are org-scoped in shared namespaces with table permissions and policy checks
Role Hierarchy
Organization Roles
| Role | Description |
|---|---|
owner | Super admin. Full org access, owner-only boundaries, logs |
admin | Full organization access, can manage members |
member | Standard member, project access based on assignments |
viewer | Read-only member |
Organization owners and admins have full project access across the organization.
Project Roles
| Role | Permissions |
|---|---|
project_owner | Full access, can delete the project and manage roles |
project_maintainer | Full access, can manage project members |
project_contributor | Create, update, delete entities within the project |
project_viewer | Read-only access to project resources |
Role Inheritance:
project_owner > project_maintainer > project_contributor > project_viewerHigher roles include all lower role permissions.
Access Control
Project Access Check
Every request is validated against an effective role:
1. Resolve user from JWT or API key
2. Check organization membership
3. Calculate the effective project role:
- Org owner or admin? -> project_owner
- Direct project role? -> that role
- Team membership? -> highest team role
- Public project? -> project_viewer
4. Compare against the required role
5. Allow, or deny with a structured 403Effective Role Calculation
The effective project role is the maximum of:
- Org owner or admin - resolves to
project_owner - Direct assignment - the role recorded for the user on the project
- Team membership - the highest role from the user's team memberships
- Public access -
project_viewerif the project is public
The resolved role is then compared against the role the route requires.
Permission Dependencies
| Action | Minimum Role |
|---|---|
| Read project | project_viewer |
| Create entities | project_contributor |
| Update entities | project_contributor |
| Delete entities | project_contributor |
| Manage project settings | project_maintainer |
| Manage project members | project_maintainer |
| Delete project | project_owner |
| Transfer ownership | project_owner |
API Authorization
Dependency Functions
Organization-level access is gated with require_org_role. Project-level access uses require_project_role and its convenience shortcuts require_project_read, require_project_write, and require_project_admin.
from sibyl.auth.dependencies import require_org_role
from sibyl.auth.authorization import (
require_project_read,
require_project_write,
)
from sibyl_core.auth import OrganizationRole
@router.get("/projects/{project_id}/entities")
async def list_entities(
project_id: str,
_project = Depends(require_project_read()), # Requires project_viewer or higher
):
...
@router.post("/projects/{project_id}/entities")
async def create_entity(
project_id: str,
_project = Depends(require_project_write()), # Requires project_contributor or higher
):
...
@router.get("/admin/system")
async def admin_only(
_: None = Depends(require_org_role(OrganizationRole.OWNER, OrganizationRole.ADMIN)),
):
...require_project_read admits project_viewer and above, require_project_write admits project_contributor and above, and require_project_admin admits project_maintainer and above.
Error Response (403 Forbidden)
When authorization fails, a structured error is returned:
{
"error": "forbidden",
"code": "PROJECT_ACCESS_DENIED",
"message": "Insufficient permissions for project",
"details": {
"project_id": "proj_abc123",
"required_role": "project_contributor",
"actual_role": "project_viewer"
}
}Error Codes:
| Code | Description |
|---|---|
PROJECT_ACCESS_DENIED | User lacks required project role |
PROJECT_NOT_FOUND | Project doesn't exist or no access |
ORG_ACCESS_DENIED | User not in organization |
Organization Isolation
Sibyl's default runtime is SurrealDB-native. Graph memory is physically isolated with a namespace per organization. Content and auth records use shared namespaces, scoped by organization_id, table permissions, and API policy checks.
Namespace-Per-Org
Each organization gets its own SurrealDB graph namespace, named org_<uuid_hex>.
- Every authenticated request resolves an organization first. Graph operations route into that organization's namespace.
- A graph query issued in one namespace cannot see another organization's graph data. Cross-org graph leakage is not possible at the storage layer.
- The SurrealDB driver is cloned per organization (
driver.clone(group_id)) so a single client instance is never shared across namespaces.
Shared Runtime Namespaces
Content tables such as raw_captures, document_chunks, and import state live in the shared sibyl_content/content namespace. Auth tables live in sibyl_auth/auth. These records are isolated with explicit organization_id predicates, SurrealDB table permissions, and API authorization checks. That is not the same as graph namespace isolation, so user-facing claims should describe content and auth as org-scoped rather than physically namespace-isolated.
Application Scope
Application code always carries organization context. Graph operations require an explicit group_id, and there is no implicit default:
from sibyl_core.graph import EntityManager
manager = EntityManager(client, group_id=str(org.id))Forgetting the organization scope routes a graph query to the wrong namespace or fails outright. Content and auth queries must include the resolved organization predicate so shared tables do not cross tenants.
PostgreSQL and Migration
PostgreSQL is retained only for migration and archive rehearsal, not for the default runtime. Where PostgreSQL is used for rehearsal, row-level security policies provide org isolation within that database. Migration and archive operations use explicit sibyld migrate commands:
sibyld migrate import migration-archive.tar.gz \
--source-type legacy-archive \
--target-mode postgres-rehearsal \
--restore-database-dump \
--yesProject Members API
Add Member
POST /api/projects/{project_id}/membersRequest:
{
"user_id": "user-uuid",
"role": "writer"
}Required Role: admin
Update Member Role
PATCH /api/projects/{project_id}/members/{member_id}Request:
{
"role": "admin"
}Required Role: admin (cannot demote/remove owners without being owner)
Remove Member
DELETE /api/projects/{project_id}/members/{member_id}Required Role: admin
List Members
GET /api/projects/{project_id}/membersRequired Role: reader
Teams
Teams provide group-based access control.
Team Membership
Users inherit the highest role from their team memberships:
User A -> Team Alpha (project_contributor) -> Project X
-> Team Beta (project_maintainer) -> Project X
Result: User A has project_maintainer on Project XCreating Teams
POST /api/organizations/{org_id}/teamsRequest:
{
"name": "Engineering",
"description": "Core engineering team"
}Team Project Access
POST /api/teams/{team_id}/projectsRequest:
{
"project_id": "proj-uuid",
"role": "project_contributor"
}All team members inherit this role for the project.
Security Considerations
Defense in Depth
- Authentication - JWT or API key validates identity
- Authorization - Role checks validate permissions
- Graph namespace isolation - SurrealDB enforces per-org graph isolation at the storage layer
- Shared-table scoping - content and auth queries carry organization predicates, table permissions, and API policy checks
Even if application code has a bug, the graph namespace boundary prevents cross-org graph access. Shared content and auth paths must preserve scoped predicates and table permissions to maintain the same tenant boundary.
Audit Logging
Permission changes are logged:
{
"action": "project_member_added",
"actor_id": "admin-user-uuid",
"target_id": "new-member-uuid",
"project_id": "proj-uuid",
"role": "project_contributor",
"timestamp": "2026-05-16T12:00:00Z"
}Principle of Least Privilege
- Default to
project_viewerfor new project members - Require explicit elevation to
project_contributororproject_maintainer - Only project creators get
project_owner
CLI Authentication
The CLI stores credentials securely:
- Location:
~/.sibyl/auth.json - File permissions:
0600(user read/write only) - Directory permissions:
0700(user only) - Atomic writes: Prevents credential file corruption
# Login
sibyl auth login
# Check auth status
sibyl auth status
# Clear stored credentials
sibyl auth clear-tokenRelated
- auth-jwt.md - JWT session authentication
- auth-api-keys.md - API key authentication
- index.md - API overview
