How to Configure Multi-Tenant Isolation and Permissions in OpenViking
OpenViking enforces multi-tenant isolation by mapping every request to a RequestContext containing an account_id, then scoping all file-system operations to that tenant's namespace while restricting administrative actions through role-based dependencies.
OpenViking treats each account (also referred to as a workspace) as an isolated tenant. When you configure multi-tenant isolation and permissions in OpenViking, you enable a security model where data paths are automatically prefixed with the tenant identifier and API access is gated by hierarchical roles. This guide walks through the configuration files, identity resolution logic, and administrative workflows required to deploy a secure, multi-tenant OpenViking cluster.
Understanding OpenViking's Tenant Architecture
OpenViking's permission model centers on three core objects defined in openviking/server/identity.py:
Role– An enumeration ofROOT,ADMIN, andUSER.ResolvedIdentity– Contains theaccount_id,user_id,agent_id, androle.RequestContext– Injected into every route; provides theaccount_idproperty used for tenant isolation.
When a request arrives, openviking/server/auth.py resolves the API key (from the X-API-Key or Authorization: Bearer header) into a ResolvedIdentity. The server then builds a RequestContext that travels through all subsequent layers. According to the OpenViking source code, this context is the single source of truth for determining which tenant directory a user may access.
Enabling Multi-Tenant Mode
Multi-tenant isolation is activated by defining a root_api_key in the server configuration. Without this key, OpenViking runs in development mode with full ROOT privileges for all requests.
Create or edit ~/.openviking/ov.conf:
{
"server": {
"host": "0.0.0.0",
"port": 1933,
"root_api_key": "my-secret-root-key",
"cors_origins": ["*"]
}
}
Start the server:
openviking-server --config ~/.openviking/ov.conf
The presence of server.root_api_key forces the authentication flow described in docs/en/guides/01-configuration.md. Every subsequent request must present a valid API key, and the server will reject unauthenticated traffic with a 401 Unauthorized error.
Managing Tenants and Users via the Admin API
Once multi-tenant mode is active, tenant lifecycle management is performed through the Admin API. These endpoints are protected by the require_role dependency factory defined in openviking/server/auth.py (lines 69‑85), which raises PermissionDeniedError if the caller's role is insufficient.
Creating Accounts (ROOT Only)
Only a caller with ROOT role may create new accounts. This operation also creates the first admin user for that tenant:
curl -X POST http://localhost:1933/api/v1/admin/accounts \
-H "X-API-Key: my-secret-root-key" \
-H "Content-Type: application/json" \
-d '{"account_id": "acme", "admin_user_id": "alice"}'
The response includes the admin user's API key (e.g., "user_key": "abcd…"). Under the hood, APIKeyManager.create_account() in openviking/server/api_keys.py (lines 18‑33) persists the account metadata and user index to the AGFS storage layer at /local/{account_id}/_system/users.json.
Registering Users (ADMIN or ROOT)
Within an existing account, an ADMIN or ROOT user can register additional users:
curl -X POST http://localhost:1933/api/v1/admin/accounts/acme/users \
-H "X-API-Key: <alice-admin-key>" \
-H "Content-Type: application/json" \
-d '{"user_id": "bob", "role": "user"}'
The new user receives a USER role by default, restricting them from administrative endpoints.
Managing Roles and Keys
Changing roles (ROOT only):
curl -X PUT http://localhost:1933/api/v1/admin/accounts/acme/users/bob/role \
-H "X-API-Key: my-secret-root-key" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
Regenerating API keys (invalidates the old key immediately):
curl -X POST http://localhost:1933/api/v1/admin/accounts/acme/users/bob/key \
-H "X-API-Key: my-secret-root-key"
These operations are handled by APIKeyManager methods in openviking/server/api_keys.py, which maintain an in-memory index for O(1) lookup while persisting changes to AGFS.
Enforcing Isolation and Permissions
Data Isolation via RequestContext
After authentication, OpenViking constructs a RequestContext that travels through the storage layer. The file-system implementation in openviking/storage/viking_fs.py uses the context's account_id to prefix every VFS URI with the tenant namespace:
viking://{account_id}/path/to/file
This ensures that a user from account acme cannot construct a request that accesses viking://competitor/.... The server automatically scopes all operations to the authenticated tenant's directory.
Role-Based Access Control
Permission checks are centralized in openviking/server/auth.py through the require_role dependency factory. Router endpoints declare their required role like this:
from openviking.server.auth import require_role
from openviking.server.identity import Role
@app.post("/api/v1/admin/accounts")
async def create_account(
ctx: RequestContext = Depends(get_request_context),
_: None = Depends(require_role(Role.ROOT)) # Only ROOT may call this
):
...
If a USER or ADMIN attempts to call a ROOT-only endpoint, the dependency raises PermissionDeniedError before the route handler executes. This pattern is applied consistently across the Admin API surface.
Practical Examples
Python SDK Workflow
The examples/multi_tenant/admin_workflow.py script demonstrates the complete lifecycle. Here is the annotated pattern:
import openviking as ov
# Step 1: Authenticate as ROOT to create a tenant
root_client = ov.SyncHTTPClient(
url="http://localhost:1933",
api_key="my-secret-root-key",
agent_id="admin-agent"
)
# Create account 'acme' with admin user 'alice'
resp = root_client.post(
"/api/v1/admin/accounts",
json={"account_id": "acme", "admin_user_id": "alice"}
)
admin_key = resp["result"]["user_key"]
# Step 2: As ADMIN, create a regular user
admin_client = ov.SyncHTTPClient(
url="http://localhost:1933",
api_key=admin_key,
agent_id="admin-agent"
)
resp = admin_client.post(
"/api/v1/admin/accounts/acme/users",
json={"user_id": "bob", "role": "user"}
)
bob_key = resp["result"]["user_key"]
# Step 3: Access data as Bob (automatically scoped to 'acme')
bob_client = ov.SyncHTTPClient(
url="http://localhost:1933",
api_key=bob_key,
agent_id="bob-agent"
)
files = bob_client.fs.ls(uri="viking://")
print(files) # Lists only paths within viking://acme/...
CLI Workflow
The examples/multi_tenant/admin_workflow.sh provides equivalent shell commands:
# Create account as ROOT
openviking admin create-account acme --admin alice --root-key my-secret-root-key
# Register user as ADMIN (using alice's key from previous step)
openviking admin register-user acme bob --role user --api-key <alice-key>
# List files as Bob
openviking --api-key <bob-key> fs ls viking://
All CLI commands invoke the same REST endpoints described in docs/en/guides/04-authentication.md.
Summary
- Enable multi-tenant mode by setting
server.root_api_keyinov.conf; without this key, the server runs in insecure development mode. - Create accounts using the Admin API with ROOT credentials; each account becomes an isolated tenant with its own namespace.
- Manage users by assigning ADMIN or USER roles; only ROOT can change roles or regenerate keys.
- Enforce isolation through the
RequestContextobject, which automatically scopes all file-system URIs toviking://{account_id}/…. - Protect endpoints using the
require_roledependency inopenviking/server/auth.py, which raisesPermissionDeniedErrorfor unauthorized access attempts.
Frequently Asked Questions
What is the difference between ROOT, ADMIN, and USER roles in OpenViking?
ROOT is the global superuser configured via root_api_key who can create accounts, manage any user across all tenants, and change roles. ADMIN is a per-account manager who can create users within their own account but cannot access other accounts or elevate privileges. USER is a standard role that can perform file operations within their tenant but cannot invoke Admin API endpoints. The require_role dependency in openviking/server/auth.py enforces these boundaries.
How does OpenViking prevent one tenant from accessing another tenant's data?
OpenViking implements data isolation by injecting a RequestContext into every request handler. This context contains the authenticated account_id, which the storage layer (openviking/storage/viking_fs.py) uses to prefix every VFS URI. For example, a request to viking://bucket/file from account acme is internally resolved to viking://acme/bucket/file. Because the server constructs this path internally using the authenticated context, users cannot craft requests that escape their tenant namespace.
Can I switch an existing user from USER to ADMIN without regenerating their API key?
Yes. Role changes do not invalidate the existing API key. As ROOT, you can call the role update endpoint to promote a user:
curl -X PUT http://localhost:1933/api/v1/admin/accounts/{account_id}/users/{user_id}/role \
-H "X-API-Key: <root-key>" \
-d '{"role": "admin"}'
The APIKeyManager in openviking/server/api_keys.py updates the role in the persistent store (/local/{account_id}/_system/users.json) and refreshes the in-memory index immediately, while preserving the existing key string.
Where are tenant and user records stored in OpenViking?
Account and user metadata are persisted to the AGFS (OpenViking's storage layer) under a system path template defined in openviking/server/api_keys.py. Specifically, user records are stored at /local/{account_id}/_system/users.json. The APIKeyManager maintains an in-memory index for O(1) lookup during request authentication, but the authoritative state resides in the file system, ensuring durability across server restarts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →