Permission API
Permission API is a Role-Based Access Control (RBAC) service that allows third-party applications to manage user roles and permissions. You can define multiple Roles, each containing Allow and Deny permission sets, and then assign roles to users. When a user has multiple roles, they are merged using Priority — higher-priority roles override lower-priority ones.
Overview
| Item | Description |
|---|---|
| Base Path | /permission/api/ |
| Authentication | Basic (app mode) or Bearer (user mode) |
| Data Format | JSON |
| Role Limit | Up to 50 roles per application |
| Permission Limit | Up to 200 allow + deny entries combined per role |
Authentication
Permission API supports two authentication modes via the Authorization header:
App Mode (Basic)
Authenticate using the application's client_id and client_secret. App mode can perform all role and user management operations.
Authorization: Basic base64(client_id:client_secret)⚠️ Security Warning: The
client_secretgrants full administrative access to your application. Never embed it in client-side applications distributed to users — secrets can be extracted through reverse engineering. App mode should only be used in trusted server environments or internal development tools.Recommended Architecture: User-facing clients should use Bearer mode (user OIDC login) combined with Cloud Functions and Permission Groups for business logic.
User Mode (Bearer)
Authenticate using the user's OAuth access_token. In user mode, available operations depend on the role's Visibility settings — see the "Visibility & Public Operations" section below.
Authorization: Bearer <access_token>🔒 Access Note: User mode access depends on the role's public settings. Roles are private by default — users cannot access them via API directly. To allow client-side calls, enable the corresponding public access toggles in the developer dashboard. It is recommended to use cloud functions for permission logic.
App mode (Basic auth) is not restricted by visibility and can always perform all operations.
Replay Protection
All API requests must include the following headers to prevent replay attacks:
| Header | Description |
|---|---|
X-Timestamp | Current Unix timestamp (seconds), must be within ±5 minutes of server time |
X-Nonce | Random unique string, 16-64 alphanumeric characters, different for each request |
Core Concepts
Roles
A role is a collection of permissions with the following properties:
| Property | Type | Description |
|---|---|---|
name | string | Role name, unique within the application |
description | string | Role description |
allow | string[] | List of allowed permission identifiers |
deny | string[] | List of denied permission identifiers |
priority | number | Priority level; higher values mean higher priority, used for conflict resolution during merge |
visibility | number | Visibility bitmask (see below) |
publicMemberOp | string | Public member operation scope: add / remove / all |
publicPermOp | string | Public permission operation scope: add / remove / all |
Permissions
Permission identifiers are custom strings defined by your application (recommended format: resource.action, e.g., user.read, post.write).
Allow / Deny Model
Each role can define both allow and deny permissions. When a user has multiple roles, the system uses a priority-based merge algorithm to compute the final permissions:
- Sort all the user's roles by
priorityfrom highest to lowest - For each permission identifier, the first role to declare it (whether in allow or deny) has the final say
- Lower-priority declarations of the same permission are ignored
Example:
Role A (priority: 10)
allow: ["read", "write"]
deny: ["delete"]
Role B (priority: 5)
allow: ["delete", "admin"]
deny: ["write"]Merged result:
read→ allowed (Role A allow, priority 10)write→ allowed (Role A allow, priority 10; Role B deny ignored)delete→ denied (Role A deny, priority 10; Role B allow ignored)admin→ allowed (only declared by Role B, priority 5)
Permission check logic: Check merged deny list first (hit → denied), then allow list (hit → allowed), neither → denied.
Visibility & Public Operations
Roles are private by default — only app mode (Basic) can operate on them. By setting the visibility bitmask, you can progressively expose operations to user mode (Bearer):
Public Permission Check Toggle
In addition to role-level visibility controls, there is an application-level toggle controlling access to permission query APIs:
| Toggle | Description |
|---|---|
| Public Permission Check | Controls whether user mode (Bearer) can call "Get Merged Permissions" and "Check Permission" endpoints |
- Disabled (default): User mode cannot call permission query endpoints. Use cloud function
$permission.checkPermission()/$permission.getUserPermissions()instead. - Enabled: User mode can directly call
GET /users/{uid}/permissionsandGET /users/{uid}/check.
💡 Recommended: Use cloud functions for permission check logic. Only enable this toggle when client-side direct queries are truly needed.
Role Visibility Bitmask
| Bit | Value | Meaning |
|---|---|---|
VIS_PUBLIC | 1 | Public role: user mode can view (list, get details) |
VIS_EDIT_MEMBER | 2 | Public member editing: user mode can assign/remove users (limited by publicMemberOp) |
VIS_EDIT_PERM | 4 | Public permission editing: user mode can modify role's allow/deny (limited by publicPermOp) |
visibility is a combination of bitmask values:
| Value | Meaning |
|---|---|
0 | Fully private (default) |
1 | Public read-only |
3 | Public + member editing |
7 | Public + member editing + permission editing |
publicMemberOp and publicPermOp further refine the scope of public operations:
| Value | Meaning |
|---|---|
add | Allow add operations only |
remove | Allow remove operations only |
all | Allow both add and remove operations |
Role Management
Roles are created and managed through the developer management panel. To dynamically control role assignments and permissions at runtime, use the $permission bridge object in Cloud Functions.
API Endpoints
List All Roles
GET /permission/api/rolesIn user mode, only roles with
visibilityincludingVIS_PUBLICare returned.
Response:
{
"status": "ok",
"roles": [
{
"name": "admin",
"description": "Administrator",
"allow": ["user.read", "user.write", "post.manage"],
"deny": [],
"priority": 100,
"visibility": 0,
"publicMemberOp": "all",
"publicPermOp": "all"
},
{
"name": "member",
"description": "Regular member",
"allow": ["user.read", "post.read"],
"deny": ["post.delete"],
"priority": 0,
"visibility": 3,
"publicMemberOp": "add",
"publicPermOp": "all"
}
]
}Get Role Details
GET /permission/api/roles/{roleName}In user mode, the role must be public (
visibility & 1).
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
roleName | string | Role name |
Response:
{
"status": "ok",
"role": {
"name": "admin",
"description": "Administrator",
"allow": ["user.read", "user.write", "post.manage"],
"deny": [],
"priority": 100,
"visibility": 0,
"publicMemberOp": "all",
"publicPermOp": "all"
}
}List Role Members
GET /permission/api/roles/{roleName}/members?page=1&limit=100In user mode, the role must be public (
visibility & 1). App mode returns email, activation state, and assignment metadata. User mode only returnsuid,rid, and assignment time to avoid exposing management fields.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
roleName | string | Role name |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
page | number | Page number, default 1 |
limit | number | Items per page, default 100, maximum 200 |
Response:
{
"status": "ok",
"roleName": "member",
"total": 2,
"page": 1,
"limit": 100,
"members": [
{
"uid": "94f48446-57ab-11f1-aa60-00163e15156d",
"rid": "SmallMoonSet",
"email": "user@example.com",
"authState": 1,
"assignedByUid": null,
"assignedByRid": null,
"assignedAt": "1780219728000"
}
]
}Get User's Roles
GET /permission/api/users/{uid}/rolesIn user mode, only public roles are returned.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
uid | string | User uid |
Response:
{
"status": "ok",
"roles": [
{
"name": "member",
"description": "Regular member",
"allow": ["user.read", "post.read"],
"deny": ["post.delete"],
"priority": 0,
"visibility": 3,
"assignedBy": "app",
"assignedAt": "2025-01-15T08:30:00.000Z"
}
]
}In user mode, the response additionally includes
visibility,assignedBy(assignment source), andassignedAt(assignment time) fields.
Get User's Merged Permissions
Returns the final allow and deny lists after priority-based merging of all the user's roles.
🔒 Access Control: Calling this endpoint in user mode (Bearer) requires the "Public Permission Check" toggle to be enabled. When disabled, use cloud function
$permission.getUserPermissions(uid)instead.
GET /permission/api/users/{uid}/permissionsPath Parameters:
| Parameter | Type | Description |
|---|---|---|
uid | string | User uid |
Response:
{
"status": "ok",
"allow": ["user.read", "user.write", "post.manage", "post.read"],
"deny": ["post.delete"]
}Check User Permission
Check logic: merged deny hit → false, allow hit → true, neither → false.
🔒 Access Control: Calling this endpoint in user mode (Bearer) requires the "Public Permission Check" toggle to be enabled. When disabled, use cloud function
$permission.checkPermission(uid, permission)instead.
GET /permission/api/users/{uid}/check?permission={permission}Path Parameters:
| Parameter | Type | Description |
|---|---|---|
uid | string | User uid |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
permission | string | Permission identifier to check |
Response:
{
"status": "ok",
"hasPermission": true
}Assign Role to User
In user mode, the role must have
visibility & VIS_EDIT_MEMBER, andpublicMemberOpmust beaddorall.
POST /permission/api/users/{uid}/roles
Content-Type: application/jsonPath Parameters:
| Parameter | Type | Description |
|---|---|---|
uid | string | User uid |
Request Body:
{
"roleName": "member"
}Response:
{
"status": "ok"
}Remove User Role
In user mode, the role must have
visibility & VIS_EDIT_MEMBER, andpublicMemberOpmust beremoveorall.
DELETE /permission/api/users/{uid}/roles/{roleName}Path Parameters:
| Parameter | Type | Description |
|---|---|---|
uid | string | User uid |
roleName | string | Role name |
Response:
{
"status": "ok"
}Update Role Permissions
In user mode, the role must have
visibility & VIS_EDIT_PERM, andpublicPermOpcontrols the allowed operations (adding/removing permission entries).
PUT /permission/api/roles/{roleName}/permissions
Content-Type: application/jsonPath Parameters:
| Parameter | Type | Description |
|---|---|---|
roleName | string | Role name |
Request Body:
{
"allow": ["user.read", "post.read", "post.write"],
"deny": ["post.delete"]
}Response:
{
"status": "ok"
}Error Responses
All error responses follow a unified format:
{
"error": "error_code",
"error_description": "Human-readable error message"
}Common Error Codes:
| Error Code | HTTP Status | Description |
|---|---|---|
unauthorized | 401 | Missing Authorization header |
invalid_client | 401 | Invalid Basic credentials |
invalid_token | 401 | Invalid Bearer token |
forbidden | 403 | Role not visible or operation not allowed in user mode |
service_not_enabled | 403 | Permission service not enabled for this application |
not_found | 404 | Role not found or user does not have this role |
duplicate | 400 | User already has this role |
limit_exceeded | 400 | allow + deny combined exceeds 200 entry limit |
Usage Examples
Typical Game Permission Design
Role: admin (priority: 100, visibility: 0)
allow: player.ban, player.kick, inventory.manage, chat.moderate
deny: (none)
Role: moderator (priority: 50, visibility: 1)
allow: player.kick, chat.moderate
deny: player.ban
Role: muted (priority: 80, visibility: 3, publicMemberOp: add)
allow: (none)
deny: chat.send, chat.color
Role: vip (priority: 10, visibility: 1)
allow: chat.color, inventory.extra_slots
deny: (none)If a user has both muted (priority 80) and vip (priority 10) roles:
chat.color→ denied (muted deny priority 80 > vip allow priority 10)inventory.extra_slots→ allowed (only declared by vip)chat.send→ denied (muted deny)
Server-Side Permission Check
// Check if a player has ban permission
const res = await fetch(
`${API_BASE}/permission/api/users/${playerUid}/check?permission=player.ban`,
{ headers: { Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}` } }
)
const { hasPermission } = await res.json()
if (!hasPermission) {
throw new Error('You do not have permission to ban players')
}Using $permission in Cloud Functions
// Use the $permission bridge object inside cloud functions
const roles = await $permission.getUserRoles(uid)
const perms = await $permission.getUserPermissions(uid)
const canBan = await $permission.checkPermission(uid, 'player.ban')
// Update role permissions (requires app-level access to the role)
await $permission.updateRolePermissions('moderator', {
allow: ['player.kick', 'chat.moderate', 'chat.pin'],
deny: ['player.ban']
})