Seat Management
Overview
Seat Management is the core permission system that controls access to resources within the Neo platform. Seats are role-based access control (RBAC) entities that define what permissions users have within specific spaces. Users are assigned to Seats through OIDC authentication, and their permissions are validated on each API request.
Key Concepts
- Seat: A named role that defines a set of permissions and can be assigned to one or more spaces
- Space: A logical container that isolates resources and permissions
- Permission: A string identifier (e.g.,
trainings:create) that grants access to specific operations - Wildcard Permission: The
*permission grants all operations - Multi-Space Seats: A single Seat can be assigned to multiple spaces, granting the same permissions across all assigned spaces
- Multiple Seats per Space: Users can have multiple Seats in the same space - the system checks ALL seats and grants access if ANY seat has the required permission
How It Works
- Users authenticate via OIDC and receive a token with
memberOfclaims containing Seat names - The system maps OIDC Seat names to internal Seats stored in MongoDB
- For each Seat, the system generates
hash_permentries for all spaces the Seat belongs to (format:{space_id}:{seat_name}) - Seat permissions are cached in Redis as a hash for fast lookups
- On each API request, the system validates permissions by:
- Extracting the space ID from the
Spacerequest header - Finding all matching Seats in the user's JWT
hash_permarray for that space - Checking if ANY of the matching Seats has the required permission (users can have multiple seats in the same space)
- Granting access if at least one Seat has the permission or wildcard (
*)
- Extracting the space ID from the
Table of Contents
API Reference
The Seat Management API provides full CRUD (Create, Read, Update, Delete) operations for managing Seats.
Create Seat
Creates a new Seat with specified permissions for one or more spaces.
Endpoint: POST /v1/seats
Request Body:
{
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456"],
"permissions": ["trainings:create", "trainings:list", "trainings:get"]
}
Response: 200 OK
{
"id": "seat-abc123",
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456"],
"permissions": ["trainings:create", "trainings:list", "trainings:get"],
"created_by": "user-123",
"created_at": "2024-01-15T10:30:00Z"
}
Get Seat
Retrieves a specific Seat by its ID.
Endpoint: GET /v1/seats/{seat_id}
Response: 200 OK
{
"id": "seat-abc123",
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456"],
"permissions": ["trainings:create", "trainings:list", "trainings:get"],
"created_by": "user-123",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
List Seats
Lists all Seats with pagination support.
Endpoint: GET /v1/seats?limit={limit}&after={after}
Query Parameters:
limit(optional): Number of seats to return (default: 20)after(optional): Cursor for pagination (seat ID)
Response: 200 OK
{
"data": [
{
"id": "seat-abc123",
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456"],
"permissions": ["trainings:create", "trainings:list", "trainings:get"],
"member_count": 5,
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": "seat-def456",
"name": "TrainingAdmin",
"spaces": ["space-123"],
"permissions": ["*"],
"member_count": 2,
"created_at": "2024-01-15T09:00:00Z"
}
],
"has_more": false,
"count": 2
}
Update Seat
Updates an existing Seat's name, spaces, or permissions. All fields are optional - only provided fields will be updated.
Endpoint: PUT /v1/seats/{seat_id}
Request Body:
{
"name": "SeniorTrainingDeveloper",
"spaces": ["space-123", "space-456", "space-789"],
"permissions": ["trainings:create", "trainings:delete", "trainings:update", "trainings:get", "trainings:list"]
}
Response: 200 OK
{
"id": "seat-abc123",
"name": "SeniorTrainingDeveloper",
"spaces": ["space-123", "space-456", "space-789"],
"permissions": ["trainings:create", "trainings:delete", "trainings:update", "trainings:get", "trainings:list"],
"created_by": "user-123",
"created_at": "2024-01-15T10:30:00Z",
"updated_by": "user-456",
"updated_at": "2024-01-15T11:00:00Z"
}
Delete Seat
Deletes a Seat. This will invalidate all users assigned to that Seat.
Endpoint: DELETE /v1/seats/{seat_id}
Response: 204 No Content
Deleting a Seat will invalidate all users assigned to that Seat. They will need to be reassigned through the OIDC provider.
Data Models
Seat Model
The core Seat data structure used throughout the system.
type Seat struct {
ID string `json:"id" bson:"id"`
Name string `json:"name" bson:"name"`
Spaces []string `json:"spaces" bson:"spaces"`
Permissions []permissions.Permission `json:"permissions" bson:"permissions"`
CreatedBy string `json:"created_by" bson:"created_by"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedBy *string `json:"updated_by,omitempty" bson:"updated_by,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"`
}
Fields:
id: Unique identifier for the Seatname: Seat name (must be unique - used to match OIDCmemberOfclaims)spaces: Array of Space IDs this Seat belongs to (a Seat can belong to multiple spaces)permissions: Array of permission strings (e.g.,["trainings:create", "trainings:list"])created_by: User ID who created the Seatcreated_at: Timestamp when the Seat was createdupdated_by: User ID who last updated the Seat (optional)updated_at: Timestamp when the Seat was last updated (optional)
OIDC Token Structure
The token structure received from the OIDC provider during authentication.
{
"sub": "user123",
"email": "[email protected]",
"memberOf": [
"seat-developer",
"seat-viewer",
"seat-admin"
],
"exp": 1234567890,
"iat": 1234564290
}
Key Fields:
sub: User subject identifiermemberOf: Array of Seat names the user belongs to (from OIDC provider)exp: Token expiration timestampiat: Token issued at timestamp
JWT Structure (Internal)
The internal JWT structure generated after OIDC authentication, used for API requests.
{
"sub": "user123",
"email": "[email protected]",
"hash_perm": [
"space-123:seat-developer",
"space-456:seat-admin",
"space-789:seat-viewer"
],
"exp": 1234567890,
"iat": 1234564290
}
Key Fields:
hash_perm: Array of space-seat mappings in format{space_id}:{seat_name}- This structure enables fast permission lookups by combining space and seat information
Architecture
MongoDB Storage
Seats are persisted in MongoDB as the source of truth.
Collection: seats
Document Structure:
{
"_id": ObjectId("..."),
"id": "seat-abc123",
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456"],
"permissions": ["trainings:create", "trainings:list", "trainings:get"],
"created_by": "user-123",
"created_at": ISODate("2024-01-15T10:30:00Z"),
"updated_by": "user-456",
"updated_at": ISODate("2024-01-15T11:00:00Z")
}
Indexes:
name(unique): Ensures Seat names are unique (used to match OIDCmemberOfclaims)spaces: Enables fast filtering by space
Redis Cache
Seat permissions are cached in Redis as a hash for high-performance permission lookups during request validation.
Key Format: seat:{seat_name}
Structure: Redis Hash with permission strings as fields
Example Redis Entries:
Key: seat:TrainingDeveloper
Hash Fields:
trainings:create → "1"
trainings:list → "1"
trainings:get → "1"
Key: seat:TrainingAdmin
Hash Fields:
* → "1" (wildcard grants all permissions)
Key: seat:DataViewer
Hash Fields:
trainings:get → "1"
trainings:list → "1"
data-models:get → "1"
data-models:list → "1"
Cache Invalidation:
- Cache is automatically updated when Seats are created, updated, or deleted
- When a Seat is updated, the Redis cache is synchronized immediately
- When a Seat is deleted, the Redis hash is removed
Use Cases & Examples
Example 1: User with Single Seat
A user assigned to a single Seat with limited permissions.
OIDC Token:
{
"sub": "alice",
"email": "[email protected]",
"memberOf": ["TrainingDeveloper"]
}
Seat Configuration:
{
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456"],
"permissions": ["trainings:create", "trainings:get"]
}
Generated JWT:
{
"sub": "alice",
"email": "[email protected]",
"hash_perm": [
"space-123:TrainingDeveloper",
"space-456:TrainingDeveloper"
]
}
Request:
GET /v1/trainings
Space: space-123
Authorization: Bearer <JWT>
Permission Validation Flow:
- Extract
spaceIdfromSpaceheader:"space-123" - Search
hash_permarray: Find"space-123:TrainingDeveloper"✅ - Extract
seat_name: Split entry →"TrainingDeveloper" - Query Redis hash
seat:TrainingDeveloper:- Check field
trainings:list→ Not found ❌ - Check field
*(wildcard) → Not found ❌
- Check field
- Required permission:
trainings:list - Result: ❌ 403 Forbidden (permission not in seat)
Example 2: User with Multiple Seats
A user assigned to multiple Seats across different spaces.
OIDC Token:
{
"sub": "bob",
"email": "[email protected]",
"memberOf": ["TrainingDeveloper", "TrainingAdmin"]
}
Seat Configurations:
[
{
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456"],
"permissions": ["trainings:create", "trainings:list", "trainings:get"]
},
{
"name": "TrainingAdmin",
"spaces": ["space-456"],
"permissions": ["*"]
}
]
Generated JWT:
{
"sub": "bob",
"email": "[email protected]",
"hash_perm": [
"space-123:TrainingDeveloper",
"space-456:TrainingDeveloper",
"space-456:TrainingAdmin"
]
}
Request 1: Accessing space-123
Request:
POST /v1/trainings
Space: space-123
Authorization: Bearer <JWT>
Permission Validation Flow:
- Extract
spaceIdfromSpaceheader:"space-123" - Search
hash_permarray: Find"space-123:TrainingDeveloper"✅ - Extract
seat_name: Split entry →"TrainingDeveloper" - Query Redis hash
seat:TrainingDeveloper:- Check field
trainings:create→ Found ✅
- Check field
- Required permission:
trainings:create - Result: ✅ 200 OK
Request 2: Accessing space-456 (Multiple Seats)
Request:
GET /v1/trainings
Space: space-456
Authorization: Bearer <JWT>
Permission Validation Flow:
- Extract
spaceIdfromSpaceheader:"space-456" - Search
hash_permarray: Find multiple entries:"space-456:TrainingDeveloper"✅"space-456:TrainingAdmin"✅
- Extract all
seat_names:["TrainingDeveloper", "TrainingAdmin"] - Query Redis for all seats:
seat:TrainingDeveloper→ Check fieldtrainings:list→ Found ✅seat:TrainingAdmin→ Check field*→ Found ✅ (wildcard grants all)
- Required permission:
trainings:list - Result: ✅ 200 OK (at least one seat has the permission)
Request 3: Accessing space-999 (No Access)
Request:
GET /v1/trainings
Space: space-999
Authorization: Bearer <JWT>
Permission Validation Flow:
- Extract
spaceIdfromSpaceheader:"space-999" - Search
hash_permarray: No entry starting with"space-999:"❌ - Result: ❌ 403 Forbidden (no Seat in this space)
Example 3: Wildcard Permission
A Seat with wildcard permissions that grants access to all operations.
Seat Configuration:
{
"name": "TrainingAdmin",
"spaces": ["space-123", "space-456"],
"permissions": ["*"]
}
Redis Cache:
Key: seat:TrainingAdmin
Hash Fields:
* → "1"
Request:
DELETE /v1/trainings/{id}
Space: space-123
Authorization: Bearer <JWT>
Permission Validation Flow:
- Extract
spaceIdfromSpaceheader:"space-123" - Search
hash_permarray: Find"space-123:TrainingAdmin"✅ - Extract
seat_name: Split entry →"TrainingAdmin" - Query Redis hash
seat:TrainingAdmin:- Check field
trainings:delete→ Not found, but... - Check field
*→ Found ✅ (wildcard grants all)
- Check field
- Required permission:
trainings:delete - Result: ✅ 200 OK (wildcard grants all permissions)
Example 4: Seat with Multiple Spaces
A single Seat assigned to multiple spaces, granting the same permissions across all spaces.
OIDC Token:
{
"sub": "charlie",
"email": "[email protected]",
"memberOf": ["TrainingDeveloper"]
}
Seat Configuration:
{
"name": "TrainingDeveloper",
"spaces": ["space-123", "space-456", "space-789"],
"permissions": ["trainings:create", "trainings:list", "trainings:get"]
}
Generated JWT:
{
"sub": "charlie",
"email": "[email protected]",
"hash_perm": [
"space-123:TrainingDeveloper",
"space-456:TrainingDeveloper",
"space-789:TrainingDeveloper"
]
}
Request to space-456:
POST /v1/trainings
Space: space-456
Authorization: Bearer <JWT>
Permission Validation Flow:
- Extract
spaceIdfromSpaceheader:"space-456" - Search
hash_permarray: Find"space-456:TrainingDeveloper"✅ - Extract
seat_name: Split entry →"TrainingDeveloper" - Query Redis hash
seat:TrainingDeveloper:- Check field
trainings:create→ Found ✅
- Check field
- Required permission:
trainings:create - Result: ✅ 200 OK
Note: The same Seat grants the same permissions in all three spaces (space-123, space-456, and space-789).