Skip to main content

Role Assignments

Role assignments link users to roles, optionally scoped to specific organizations.

Assignment Model

interface RoleAssignment {
id: string;
userId: string;
roleId: string;
organizationId: string | null; // null = tenant-wide
expiresAt: Date | null; // null = no expiry
createdAt: Date;
createdBy: string; // Who created the assignment
}

Assign Role

Endpoint

POST /role-assignments

Request (Tenant-Wide)

{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"roleId": "tenant-admin-id"
}

Request (Organization-Scoped)

{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"roleId": "org-admin-id",
"organizationId": "engineering-org-id"
}

Request (Time-Bound)

{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"roleId": "project-lead-id",
"organizationId": "project-alpha-id",
"expiresAt": "2024-06-30T23:59:59.000Z"
}

Response (201 Created)

{
"id": "ra-001",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"roleId": "org-admin-id",
"organizationId": "engineering-org-id",
"expiresAt": null,
"createdAt": "2024-01-15T10:30:00.000Z",
"createdBy": "admin-user-id"
}

List Role Assignments

Endpoint

GET /role-assignments

Query Parameters

ParameterTypeDescription
userIdUUIDFilter by user
roleIdUUIDFilter by role
organizationIdUUIDFilter by organization

Response (200 OK)

{
"data": [
{
"id": "ra-001",
"userId": "user-1",
"roleId": "tenant-admin-id",
"organizationId": null,
"user": { "id": "user-1", "email": "admin@example.com" },
"role": { "id": "tenant-admin-id", "name": "TENANT_ADMIN" }
},
{
"id": "ra-002",
"userId": "user-2",
"roleId": "org-admin-id",
"organizationId": "engineering-id",
"user": { "id": "user-2", "email": "eng-lead@example.com" },
"role": { "id": "org-admin-id", "name": "ORG_ADMIN" },
"organization": { "id": "engineering-id", "name": "Engineering" }
}
],
"pagination": {
"total": 15,
"page": 1,
"limit": 20,
"totalPages": 1
}
}

Revoke Role

Endpoint

DELETE /role-assignments/:id

Response (204 No Content)

Update Assignment

Endpoint

PATCH /role-assignments/:id

Request

{
"expiresAt": "2024-12-31T23:59:59.000Z"
}

Bulk Assign Roles

Endpoint

POST /role-assignments/bulk

Request

{
"assignments": [
{ "userId": "user-1", "roleId": "member-id", "organizationId": "eng-id" },
{ "userId": "user-2", "roleId": "member-id", "organizationId": "eng-id" },
{ "userId": "user-3", "roleId": "member-id", "organizationId": "eng-id" }
]
}

Response (201 Created)

{
"created": 3,
"assignments": [
{ "id": "ra-010", "userId": "user-1", "roleId": "member-id" },
{ "id": "ra-011", "userId": "user-2", "roleId": "member-id" },
{ "id": "ra-012", "userId": "user-3", "roleId": "member-id" }
]
}

Assignment Rules

1. No Self-Escalation

Users cannot assign themselves a role higher than their current highest role:

{
"type": "https://api.cortex.purplelab.ai/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "Cannot assign a role with higher privileges than your own"
}

2. Organization Scope Match

Organization-scoped roles can only be assigned with an organizationId:

{
"type": "https://api.cortex.purplelab.ai/errors/validation",
"title": "Validation Error",
"status": 400,
"detail": "Organization-scoped roles require an organizationId"
}

3. Duplicate Prevention

Same user cannot have the same role in the same context twice:

{
"type": "https://api.cortex.purplelab.ai/errors/conflict",
"title": "Conflict",
"status": 409,
"detail": "User already has this role in this organization"
}

Code Examples

TypeScript

interface RoleAssignment {
id: string;
userId: string;
roleId: string;
organizationId: string | null;
expiresAt: string | null;
}

interface AssignRoleDto {
userId: string;
roleId: string;
organizationId?: string;
expiresAt?: string;
}

class RoleAssignmentClient {
constructor(private accessToken: string) {}

async assign(data: AssignRoleDto): Promise<RoleAssignment> {
const response = await fetch('http://localhost:8091/role-assignments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`,
},
body: JSON.stringify(data),
});
return response.json();
}

async revoke(assignmentId: string): Promise<void> {
await fetch(`http://localhost:8091/role-assignments/${assignmentId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${this.accessToken}` },
});
}

async listForUser(userId: string): Promise<RoleAssignment[]> {
const response = await fetch(
`http://localhost:8091/role-assignments?userId=${userId}`,
{ headers: { 'Authorization': `Bearer ${this.accessToken}` } }
);
const data = await response.json();
return data.data;
}
}

cURL

# Assign role (tenant-wide)
curl -X POST http://localhost:8091/role-assignments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access-token>" \
-d '{"userId": "user-id", "roleId": "tenant-admin-id"}'

# Assign role (organization-scoped)
curl -X POST http://localhost:8091/role-assignments \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access-token>" \
-d '{
"userId": "user-id",
"roleId": "org-admin-id",
"organizationId": "engineering-id"
}'

# Revoke role
curl -X DELETE http://localhost:8091/role-assignments/ra-001 \
-H "Authorization: Bearer <access-token>"

# List user's assignments
curl "http://localhost:8091/role-assignments?userId=user-id" \
-H "Authorization: Bearer <access-token>"