docs/en/api/12-acl.md
The ACL API manages direct grants on shared viking://resources/... nodes and reports their inherited effective permissions. Private resources do not accept ACLs and must be moved into the shared scope to be shared.
Read Resource Access Control (ACL) for the permission and inheritance model.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/acl?uri={uri} | Get direct, inherited, and effective ACLs |
| PUT | /api/v1/acl | Replace the node's direct ACL |
| DELETE | /api/v1/acl?uri={uri} | Clear the node's direct ACL |
| POST | /api/v1/acl/grant | Set one principal's direct level |
| POST | /api/v1/acl/revoke | Remove one principal's direct grant |
Every endpoint requires manage on the target node. Account ADMINs implicitly manage shared resources.
viking://resources is a fixed shared scope and cannot carry a direct ACL. The
account setting acl.enabled defaults to false. While disabled, shared
resources use the original public behavior and ACL authorization is skipped.
When enabled, newly created shared files, directories, and add-resource roots
grant the creator direct manage and inherit the parent ACL. Existing content
without an ACL remains public. Descendants within an add-resource import only
inherit the root grant.
{
"principal": "user:bob",
"level": "read"
}
| Field | Type | Description |
|---|---|---|
principal | string | user:{user_id}, group:{group_id}, or user:* |
level | string | read, write, or manage |
The caller supplies the account-unique, stable group_id through the Admin API. A group has no separate display name. After a group is deleted, its old principal no longer matches any request unless the same group_id is created again.
{
"uri": "viking://resources/project-a",
"acl_mode": "inherit",
"direct_entries": [
{"principal": "user:bob", "level": "read"}
],
"inherited_entries": [
{"principal": "group:engineering", "level": "write"}
],
"effective_entries": [
{"principal": "group:engineering", "level": "write"},
{"principal": "user:bob", "level": "read"}
]
}
| Field | Description |
|---|---|
direct_entries | Entries set directly on this node |
inherited_entries | Merged direct ACLs from all ancestors |
effective_entries | The merged direct and inherited entries |
acl_mode | none when ACL does not control the node; inherit when direct and inherited ACLs apply; read-only and derived |
The account ADMIN implicit manage permission is not included in these lists.
GET /api/v1/acl?uri={uri}
GET can report an existing target that has no context record: direct_entries is empty and inherited permissions are resolved from existing ancestor contexts. Mutating ACL endpoints require a context record for the target.
curl "http://localhost:1933/api/v1/acl?uri=viking%3A%2F%2Fresources%2Fproject-a" \
-H "X-API-Key: your-key"
Python SDK
report = client.acl_get("viking://resources/project-a")
Go SDK
report, err := client.ACL(ctx, "viking://resources/project-a")
PUT /api/v1/acl
Request body:
{
"uri": "viking://resources/project-a",
"entries": [
{"principal": "user:bob", "level": "read"},
{"principal": "group:engineering", "level": "write"}
]
}
entries completely replaces this node's direct ACL without changing direct ACLs on ancestors or descendants. Duplicate principals keep their highest level. An empty list is equivalent to deleting this node's direct ACL.
curl -X PUT http://localhost:1933/api/v1/acl \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"uri": "viking://resources/project-a",
"entries": [
{"principal": "user:bob", "level": "read"},
{"principal": "group:engineering", "level": "write"}
]
}'
Python SDK
report = client.acl_set(
"viking://resources/project-a",
[
{"principal": "user:bob", "level": "read"},
{"principal": "group:engineering", "level": "write"},
],
)
The asynchronous client uses the same method name:
report = await client.acl_set(uri, entries)
Go SDK
report, err := client.SetACL(ctx, "viking://resources/project-a", []openviking.ACLEntry{
{Principal: "user:bob", Level: "read"},
{Principal: "group:engineering", Level: "write"},
})
CLI
ov acl set viking://resources/project-a \
--entry user:bob=read \
--entry group:engineering=write
POST /api/v1/acl/grant
{
"uri": "viking://resources/project-a",
"principal": "user:bob",
"level": "write"
}
This sets Bob's direct level on the current node to write. It updates an existing direct entry without changing other principals.
curl -X POST http://localhost:1933/api/v1/acl/grant \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"uri": "viking://resources/project-a",
"principal": "user:bob",
"level": "write"
}'
report = client.acl_grant(
"viking://resources/project-a",
principal="user:bob",
level="write",
)
ov acl grant viking://resources/project-a --principal user:bob --level write
POST /api/v1/acl/revoke
{
"uri": "viking://resources/project-a",
"principal": "user:bob"
}
revoke removes only Bob's direct entry on the current node. Any permission inherited by Bob from an ancestor remains effective.
report = client.acl_revoke("viking://resources/project-a", principal="user:bob")
ov acl revoke viking://resources/project-a --principal user:bob
DELETE /api/v1/acl?uri={uri}
This does not remove direct ACLs on descendants. The current node is recalculated from its ancestors, while each descendant continues to combine its own direct ACL with its ancestors.
curl -X DELETE \
"http://localhost:1933/api/v1/acl?uri=viking%3A%2F%2Fresources%2Fproject-a" \
-H "X-API-Key: your-key"
report = client.acl_delete("viking://resources/project-a")
ov acl rm viking://resources/project-a
The API checks manage permission before confirming existence to an authorized caller, preventing resource discovery through error types.
| Scenario | Error |
|---|---|
URI is outside viking://resources/... | INVALID_ARGUMENT |
| Caller lacks manage | PERMISSION_DENIED |
| Authorized caller targets a URI that does not exist | NOT_FOUND |
| ACL mutation targets a URI without a context record | INVALID_ARGUMENT; index it first |
Invalid principal syntax or group:* | INVALID_ARGUMENT |
Level is not read/write/manage | INVALID_ARGUMENT |
Request includes read-only fields such as acl_mode | INVALID_ARGUMENT |
Direct and inherited ACL fields are both stored in context records. An update changes the target direct ACL and recalculates descendant inherited ACLs in one subtree batch; a failed write restores the previous context ACL fields.
find/search endpoints