# How to Configure Multi-Tenant Isolation and Permissions in OpenViking

> Learn to configure multi-tenant isolation and permissions in OpenViking. Secure your data by scoping file operations and restricting admin actions with role-based access control.

- Repository: [Volcengine/OpenViking](https://github.com/volcengine/OpenViking)
- Tags: how-to-guide
- Published: 2026-03-08

---

**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`](https://github.com/volcengine/OpenViking/blob/main/openviking/server/identity.py):

- **`Role`** – An enumeration of `ROOT`, `ADMIN`, and `USER`.
- **`ResolvedIdentity`** – Contains the `account_id`, `user_id`, `agent_id`, and `role`.
- **`RequestContext`** – Injected into every route; provides the `account_id` property used for tenant isolation.

When a request arrives, [`openviking/server/auth.py`](https://github.com/volcengine/OpenViking/blob/main/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`:

```json
{
  "server": {
    "host": "0.0.0.0",
    "port": 1933,
    "root_api_key": "my-secret-root-key",
    "cors_origins": ["*"]
  }
}

```

Start the server:

```bash
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`](https://github.com/volcengine/OpenViking/blob/main/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`](https://github.com/volcengine/OpenViking/blob/main/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:

```bash
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`](https://github.com/volcengine/OpenViking/blob/main/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:

```bash
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):

```bash
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):

```bash
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`](https://github.com/volcengine/OpenViking/blob/main/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`](https://github.com/volcengine/OpenViking/blob/main/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`](https://github.com/volcengine/OpenViking/blob/main/openviking/server/auth.py) through the `require_role` dependency factory. Router endpoints declare their required role like this:

```python
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`](https://github.com/volcengine/OpenViking/blob/main/examples/multi_tenant/admin_workflow.py) script demonstrates the complete lifecycle. Here is the annotated pattern:

```python
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`](https://github.com/volcengine/OpenViking/blob/main/examples/multi_tenant/admin_workflow.sh) provides equivalent shell commands:

```bash

# 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`](https://github.com/volcengine/OpenViking/blob/main/docs/en/guides/04-authentication.md).

## Summary

- **Enable multi-tenant mode** by setting `server.root_api_key` in [`ov.conf`](https://github.com/volcengine/OpenViking/blob/main/ov.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 `RequestContext` object, which automatically scopes all file-system URIs to `viking://{account_id}/…`.
- **Protect endpoints** using the `require_role` dependency in [`openviking/server/auth.py`](https://github.com/volcengine/OpenViking/blob/main/openviking/server/auth.py), which raises `PermissionDeniedError` for 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`](https://github.com/volcengine/OpenViking/blob/main/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`](https://github.com/volcengine/OpenViking/blob/main/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:

```bash
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`](https://github.com/volcengine/OpenViking/blob/main/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`](https://github.com/volcengine/OpenViking/blob/main/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.