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
| Parameter | Type | Description |
|---|---|---|
userId | UUID | Filter by user |
roleId | UUID | Filter by role |
organizationId | UUID | Filter 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>"