Skip to content

Permissions & Roles

permio.permissions.v1.PermissionsService stores permissions, roles and grants, and answers checks. Every request names a project_name. Your organisation comes from the API key.

In the tables below, Key access is the API key access level needed to make the call. Check is the default for new keys, and Full is the level that also allows writes.

RPC REST Key access
Check POST /permissions-service/v1/permissions/check Check
BatchCheck POST /permissions-service/v1/permissions/check/batch Check
CreatePermission POST /permissions-service/v1/permissions Full
GetPermission GET /permissions-service/v1/permissions/{name} Check
UpdatePermission PUT /permissions-service/v1/permissions/{name} Full
DeletePermission DELETE /permissions-service/v1/permissions/{name} Full
ListPermissions GET /permissions-service/v1/permissions Check
CreateRole POST /permissions-service/v1/roles Full
GetRole GET /permissions-service/v1/roles/{id} Check
UpdateRole PUT /permissions-service/v1/roles/{id} Full
DeleteRole DELETE /permissions-service/v1/roles/{id} Full
ListRoles GET /permissions-service/v1/roles Check
GrantUserPermissionsAndRolesOnResource POST /permissions-service/v1/permissions/assign Full
RevokeUserPermissionsAndRolesOnResource POST /permissions-service/v1/permissions/revoke Full
GetUserPermissionsAndRolesOnResource GET /permissions-service/v1/permissions/user/{user_id}/resource/{resource_uri=**} Check
ListPrincipals GET /permissions-service/v1/permissions/principals Check
GetAllAssignmentsForPrincipal GET /permissions-service/v1/permissions/assignments/principal/{principal_id=**} Check
ListAccess GET /permissions-service/v1/access Check
ListPrincipalsWithAccess GET /permissions-service/v1/access/principals Check
CreateGroup POST /permissions-service/v1/groups Full
GetGroup GET /permissions-service/v1/groups/{name} Check
UpdateGroup PUT /permissions-service/v1/groups/{name} Full
DeleteGroup DELETE /permissions-service/v1/groups/{name} Full
ListGroups GET /permissions-service/v1/groups Check
AddGroupMembers POST /permissions-service/v1/groups/{name}/members/add Full
RemoveGroupMembers POST /permissions-service/v1/groups/{name}/members/remove Full
ListGroupMembers GET /permissions-service/v1/groups/{name}/members Check
ListGroupsForPrincipal GET /permissions-service/v1/group-memberships/{principal_id=**} Check

GetTotalRolesAndPermissionsForOrganisation (GET /permissions-service/v1/permissions/organisation/{organisation_id}/totals) is also part of the service. It needs an organisation-level permission that API keys don’t have, and the dashboard uses it.

Field Rule
project_name 1–32 characters: lowercase letters, digits and -.
Permission name 1–128 characters: a-z A-Z 0-9 . _ -.
Role name 1–128 characters, no control characters.
Role id 1–128 characters: a-z A-Z 0-9 . _ -.
Group name 1–64 characters: lowercase letters, digits and -. Its principal ID is group:<name>.
Principal IDs Any non-empty string. IDs starting with group: are reserved for groups: such an ID must name a group that exists in the project, or the call fails with NOT_FOUND (INVALID_ARGUMENT if it isn’t a valid group name). Revoking is the exception.
Resource URIs See Resources and inheritance.

Permission

Field Type Description
id string Unique ID.
organisation_id string Owning organisation.
name string The name your code checks, e.g. document.read.
description string Free text.
project_name string Owning project.

Role

Field Type Description
id string Unique ID. Use it when granting and in the role endpoints.
organisation_id string Owning organisation.
name string Display name.
description string Free text.
permissions Permission[] The permissions in the role.
project_name string Owning project.

Assignment

Field Type Description
resource_uri string The resource the grant is on.
permissions Permission[] Permissions granted directly.
roles Role[] Roles granted.

POST/permissions-service/v1/permissions/check

Checks whether a principal holds every listed permission on every listed resource. Grants on ancestors of a resource count, and so do grants to the groups the principal is a member of. See how a check is decided. To get one answer per resource, use BatchCheck.

Request (CheckPermissionRequest)

Field Type Description
project_name string Required. Project to check in.
principal_id string Required. The principal being checked.
resource_uris string[] Required. 1–100 unique resource URIs.
permissions string[] Required. 1–100 unique permission names.

Response (CheckPermissionResponse)

Field Type Description
passed bool true if every permission is held on every resource.
missing_permissions MissingPermissions[] For each resource that failed: resource_uri and the missing_permissions not held there.

A failed check is a successful response with passed: false. PERMISSION_DENIED means the API key lacks permission.check on the resources or on the principal, not that the principal was denied.

const { allowed, missing } = await permio.checkDetailed({
principal: "alice",
resources: ["/documents/42", "/invoices/1"],
permission: "document.read",
});

POST/permissions-service/v1/permissions/check/batch

Checks a principal’s permissions on each resource separately and returns one result per resource, in request order. It is evaluated exactly like Check: inheritance, stored _ wildcards, roles and the principal’s groups all count, and it fails closed. A resource is allowed only if the principal holds every requested permission on it.

Needs: the same as Check: permission.check on every resource and on the principal.

Request (BatchCheckRequest)

Field Type Description
project_name string Required. Project to check in.
principal_id string Required. The principal being checked.
resource_uris string[] Required. 1–100 unique resource URIs, with the same rules as Check.
permissions string[] Required. 1–100 unique permission names.

Response (BatchCheckResponse)

Field Type Description
results BatchCheckResult[] One per requested resource, in request order.

BatchCheckResult

Field Type Description
resource_uri string The resource.
allowed bool true if the principal holds every requested permission on it.
missing_permissions string[] The requested permissions not held on it, in request order.

Errors: INVALID_ARGUMENT for a malformed request; PERMISSION_DENIED if the API key lacks permission.check; NOT_FOUND if principal_id is group:<slug> and that group doesn’t exist.

const allowed = await permio.batchCheck({
principal: "alice",
resources: ["/documents/42", "/invoices/1"],
permission: "document.read",
});
allowed.get("/documents/42"); // true

Available from SDK 0.2.0. batchCheckDetailed returns the missing permissions too. See Batch checks.

The Go, Python, Rust and Java examples on this page use a client set up as in Clients.


POST/permissions-service/v1/permissions

Creates a permission. This call is idempotent: if the permission already exists, it is updated. Returns the Permission.

Field Type Description
project_name string Required.
name string Required.
description string Optional.
const perm = await permio.permissions.createPermission({
projectName: "default",
name: "document.read",
description: "Read documents",
});
GET/permissions-service/v1/permissions/{name}?project_name=…

Returns a Permission by name, or NOT_FOUND.

const perm = await permio.permissions.getPermission({ projectName: "default", name: "document.read" });
PUT/permissions-service/v1/permissions/{name}

Updates a permission, or returns NOT_FOUND if it doesn’t exist. Returns the updated Permission.

Field Type Description
project_name string Required.
name string Required (path). The current name.
new_name string Optional. A new name; if omitted, the name stays the same.
description string The new description.
await permio.permissions.updatePermission({
projectName: "default",
name: "document.read",
description: "Read any document",
});
DELETE/permissions-service/v1/permissions/{name}?project_name=…

Deletes a permission. This call is idempotent: it succeeds whether or not the permission existed.

await permio.permissions.deletePermission({ projectName: "default", name: "document.read" });
GET/permissions-service/v1/permissions?project_name=…

Lists permissions in a project. The response has permissions (Permission[]) and pagination.

Field Type Description
project_name string Required.
pagination PaginationRequest Optional. limit (1–100) and cursor.
search string Optional. Filters by name.
for await (const p of permio.listPermissions({ search: "document." })) {
console.log(p.name);
}

POST/permissions-service/v1/roles

Creates a role from existing permissions. This call is idempotent: if the role already exists, it is updated. Returns the Role, including its id.

Field Type Description
project_name string Required.
name string Required.
description string Optional.
permissions string[] Permission names, e.g. ["document.read"].
const role = await permio.permissions.createRole({
projectName: "default",
name: "viewer",
description: "Can read documents",
permissions: ["document.read"],
});
console.log(role.id);
GET/permissions-service/v1/roles/{id}?project_name=…

Returns a Role by ID.

const role = await permio.permissions.getRole({ projectName: "default", id: roleId });
PUT/permissions-service/v1/roles/{id}

Replaces a role’s name, description and permissions, or returns NOT_FOUND. Returns the updated Role.

Field Type Description
project_name string Required.
id string Required (path).
name string Required.
description string Optional.
permissions string[] Permission names.
await permio.permissions.updateRole({
projectName: "default",
id: roleId,
name: "editor",
permissions: ["document.read", "document.write"],
});
DELETE/permissions-service/v1/roles/{id}?project_name=…

Deletes a role. This call is idempotent.

await permio.permissions.deleteRole({ projectName: "default", id: roleId });
GET/permissions-service/v1/roles?project_name=…

Lists roles in a project. The response has roles (Role[]) and pagination. It takes the same pagination and search fields as ListPermissions.

for await (const role of permio.listRoles()) {
console.log(role.id, role.name);
}

POST/permissions-service/v1/permissions/assign

Grants permissions and/or roles to a principal on a resource. Grants are additive: existing grants are kept. The grant also applies to every resource beneath resource_uri. Unknown permissions or roles are rejected with INVALID_ARGUMENT.

To grant to a group, set user_id to group:<slug>. The group must exist in the project, or the call fails with NOT_FOUND. Every member of the group then holds the grant. Grants to groups don’t count towards your unique principals.

Field Type Description
project_name string Required.
user_id string Required. The principal ID, the same value you pass as principal_id to Check.
resource_uri string Required.
permissions string[] Permission names.
roles string[] Role IDs.
await permio.permissions.grantUserPermissionsAndRolesOnResource({
projectName: "default",
userId: "alice",
resourceUri: "/documents",
roles: [roleId],
});
POST/permissions-service/v1/permissions/revoke

Removes permissions and/or roles from a principal on a resource. It takes the same fields as Grant. Revoking from a group:<slug> principal works even if the group no longer exists, so stray grants can be cleaned up.

await permio.permissions.revokeUserPermissionsAndRolesOnResource({
projectName: "default",
userId: "alice",
resourceUri: "/documents",
roles: [roleId],
});
GET/permissions-service/v1/permissions/user/{user_id}/resource/{resource_uri=**}?project_name=…
GET/permissions-service/v1/permissions/user/{user_id}/resource?resource_uri=…&project_name=…

Returns the permissions and roles assigned to a principal on one resource. The response has project_name, user_id, resource_uri, permissions (names) and roles (IDs).

In the first form, send the resource URI without its leading slash. The second form passes the URI exactly as a query parameter.

const res = await permio.permissions.getUserPermissionsAndRolesOnResource({
projectName: "default",
userId: "alice",
resourceUri: "/documents",
});
GET/permissions-service/v1/permissions/principals?project_name=…

Lists the IDs of principals with grants in the project. The response has principals (string[]) and pagination. It takes pagination and search.

for await (const id of permio.listPrincipals()) console.log(id);
GET/permissions-service/v1/permissions/assignments/principal/{principal_id=**}?project_name=…

Lists every grant a principal has in the project. The response has assignments (Assignment[]) and pagination. Principal IDs may contain slashes.

for await (const a of permio.listAssignments({ principal: "alice" })) {
console.log(a.resourceUri, a.permissions.map((p) => p.name), a.roles.map((r) => r.name));
}

These answer “where?” and “who?” instead of “may they?”, with the same rules as Check. The guide Filtering lists and building share dialogs shows how to use them.

GET/permissions-service/v1/access?project_name=…&principal_id=…&permission=…&under_uri=…

Answers “where can this principal use this permission, at or below under_uri?”. It counts grants made to the principal directly, through roles, and through the groups it is a member of. perms.io doesn’t know your records, so the answer is a list of scopes in your URI tree, not a list of objects:

  • If the principal holds the permission on under_uri itself (through a grant on it, on an ancestor, or on a stored _ wildcard URI that matches it), covers_all is true and scopes is a single SUBTREE scope for under_uri. There are no further pages.
  • Otherwise scopes lists the URIs of the grants below under_uri that confer the permission. A concrete URI is a SUBTREE scope, because a grant applies to its URI and everything below it. A URI with _ wildcard segments is a PATTERN scope. Scopes are ordered by uri and paginated.

Needs: permission.check on under_uri, and principal.assignment.get on the principal.

Request (ListAccessRequest)

Field Type Description
project_name string Required.
principal_id string Required.
permission string Required. One permission name.
under_uri string Required. Only resources at or below this URI are considered. A _ segment here is literal.
pagination PaginationRequest Optional.

Response (ListAccessResponse)

Field Type Description
covers_all bool true if the principal holds the permission on under_uri itself, and so on everything below it.
scopes AccessScope[] The places at or below under_uri where the principal holds the permission, ordered by uri.
pagination PaginationResponse next_cursor for the next page.

AccessScope

Field Type Description
uri string A resource URI (SUBTREE, EXACT) or a URI pattern with _ segments (PATTERN). For a grant below under_uri, it is the URI the grant was made on, which may have _ segments in the part that matches under_uri.
scope AccessScopeKind Which resources uri stands for.

AccessScopeKind

Value Meaning
ACCESS_SCOPE_KIND_SUBTREE The resource uri and every resource below it.
ACCESS_SCOPE_KIND_PATTERN Every resource whose leading segments match uri, where each _ segment matches exactly one segment, and every resource below those.
ACCESS_SCOPE_KIND_EXACT Exactly the resource uri. Reserved: grants are always inherited, so it isn’t returned today.

To filter a database by a SUBTREE scope p, match path = p or path starting with p/. A bare prefix match on p would also match siblings such as /documents/12 for /documents/1.

Errors: INVALID_ARGUMENT for a malformed request or an invalid cursor; PERMISSION_DENIED if the API key lacks either permission; NOT_FOUND if principal_id is group:<slug> and that group doesn’t exist.

const access = await permio.listAccess({
principal: "alice",
permission: "document.read",
under: "/documents",
});
access.coversAll; // false
access.toPrefixes(); // ["/documents/4", "/documents/7"]

Available from SDK 0.2.0. It follows every page. See listAccess.

GET/permissions-service/v1/access/principals?project_name=…&resource_uri=…

Answers “who has access to resource_uri?”. It lists the principals with a grant on the resource, on an ancestor of it, or on a stored _ wildcard URI that matches it, with the grants that give the access.

  • With permission set, only grants that confer that permission count, so a principal is listed exactly when a Check for it passes through its own grants. Without it, every grant with at least one permission or role counts.
  • Groups are listed as principals, with is_group set. With expand_groups, each group’s members are listed too, once per group, with via_group set.
  • Entries are ordered by principal_id, then via_group, and paginated.

Needs: principal.list and principal.assignment.list on the project.

Request (ListPrincipalsWithAccessRequest)

Field Type Description
project_name string Required.
resource_uri string Required. The resource to look at. A _ segment here is literal.
permission string Optional. Only count grants that confer this permission.
expand_groups bool Optional. Also list the members of every listed group.
pagination PaginationRequest Optional.

Response (ListPrincipalsWithAccessResponse): principals (PrincipalAccess[]) and pagination.

PrincipalAccess

Field Type Description
principal_id string The principal. A group appears as group:<slug>.
is_group bool true if principal_id is a group.
via_group string For entries added by expand_groups: the slug of the group through which the principal holds the grants in via. Empty for grants made to the principal itself.
via AccessGrant[] The grants that give the access, most specific first.

AccessGrant

Field Type Description
resource_uri string The URI the grant was made on: the resource itself, an ancestor, or a URI with _ segments that matches it.
inherited bool true unless the grant was made on exactly the resource.
match AccessGrantMatch ACCESS_GRANT_MATCH_EXACT (on the resource), ACCESS_GRANT_MATCH_ANCESTOR (on an ancestor, possibly with _ segments) or ACCESS_GRANT_MATCH_WILDCARD (same depth, with _ segments).
role AccessGrantRole Set when the access comes through a role: its id and name.
direct AccessGrantDirect Set (as an empty message) when the access comes from directly granted permissions.

role and direct are a oneof called source: exactly one is set.

Errors: INVALID_ARGUMENT for a malformed request or an invalid cursor; PERMISSION_DENIED if the API key lacks either permission.

const entries = await permio.whoHasAccess({
resource: "/documents/42",
permission: "document.read",
expandGroups: true,
});

Available from SDK 0.2.0. It follows every page. See whoHasAccess.


A group is a named set of principals in a project. Grant to its principal ID, group:<slug>, with GrantUserPermissionsAndRolesOnResource, and every member holds the grant. Groups can’t contain groups.

In the paths below, {name} is the group’s slug: 1–64 characters, lowercase letters, digits and -.

Group

Field Type Description
id string Unique ID.
organisation_id string Owning organisation.
project_name string Owning project.
name string The slug, unique within the project. It can’t be changed.
description string Free text, up to 1024 characters.
principal_id string The principal ID grants to the group are made to: group:<name>.
created_at, updated_at Timestamp

GroupMember: principal_id (string) and created_at (Timestamp, when the principal was added).

POST/permissions-service/v1/groups

Creates a group and returns the Group. Returns ALREADY_EXISTS if the project already has a group with this name.

Needs: group.create on the project.

Field Type Description
project_name string Required.
name string Required. The slug.
description string Optional. Up to 1024 characters.
const g = await permio.groups.create("editors", { description: "Can edit every document" });
g.principalId; // "group:editors"
GET/permissions-service/v1/groups/{name}?project_name=…

Returns a Group, or NOT_FOUND.

Needs: group.get on the group.

const g = await permio.groups.get("editors");
PUT/permissions-service/v1/groups/{name}

Replaces a group’s description and returns the updated Group, or NOT_FOUND. The name can’t be changed, because group:<name> is the principal ID its grants are stored under.

Needs: group.update on the group.

Field Type Description
project_name string Required.
name string Required (path).
description string The new description. Up to 1024 characters.
await permio.groups.update("editors", { description: "Edits documents and comments" });
DELETE/permissions-service/v1/groups/{name}?project_name=…

Deletes a group together with its memberships and every grant made to it, in one transaction. This call is idempotent: it succeeds whether or not the group existed.

Needs: group.delete on the group, and principal.assignment.delete on the project, because it revokes the group’s grants.

await permio.groups.delete("editors");
GET/permissions-service/v1/groups?project_name=…

Lists the groups in a project, ordered by name. The response has groups (Group[]) and pagination. It takes pagination and search (filters by name, up to 128 characters).

Needs: group.list on the project.

for await (const g of permio.groups.list({ search: "edit" })) console.log(g.name);
POST/permissions-service/v1/groups/{name}/members/add

Adds up to 100 principals to a group. Principals that are already members are reported, not added again. A principal that is new to your organisation counts towards its unique principals, exactly as if it had been granted something, and goes through the same billing check.

Needs: group.member.add on the group and principal.assignment.create on the project. A member gets the group’s grants anywhere in the project, so only a caller who could grant that access directly may add members.

Field Type Description
project_name string Required.
name string Required (path).
principal_ids string[] Required. 1–100 unique principal IDs. They may not start with group:.

Response (AddGroupMembersResponse)

Field Type Description
added_principal_ids string[] The principals that were added, in request order.
existing_principal_ids string[] The principals that were already members, in request order.

Errors: INVALID_ARGUMENT if a principal ID starts with group: (groups can’t be nested); NOT_FOUND if the group doesn’t exist; PERMISSION_DENIED if the caller lacks either permission, or if the organisation may not add new principals.

const { added, existing } = await permio.groups.addMembers("editors", ["alice", "bob"]);
POST/permissions-service/v1/groups/{name}/members/remove

Removes up to 100 principals from a group. Principals that aren’t members are reported. It takes the same fields as AddGroupMembers.

Needs: group.member.remove on the group and principal.assignment.create on the project.

Response (RemoveGroupMembersResponse): removed_principal_ids and not_member_principal_ids (string[], in request order).

const { removed, notMembers } = await permio.groups.removeMembers("editors", ["bob"]);
GET/permissions-service/v1/groups/{name}/members?project_name=…

Lists a group’s members, ordered by principal ID. The response has members (GroupMember[]) and pagination. It takes pagination.

Needs: group.member.list on the group.

for await (const m of permio.groups.listMembers("editors")) console.log(m.principalId);
GET/permissions-service/v1/group-memberships/{principal_id=**}?project_name=…

Lists the groups a principal is a member of in the project, ordered by name. The response has groups (Group[]) and pagination. It takes pagination. Principal IDs may contain slashes.

Needs: group.list on the project, and principal.assignment.list on the principal.

for await (const g of permio.groups.listForPrincipal("alice")) console.log(g.name);