Skip to main content

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

  1. Users authenticate via OIDC and receive a token with memberOf claims containing Seat names
  2. The system maps OIDC Seat names to internal Seats stored in MongoDB
  3. For each Seat, the system generates hash_perm entries for all spaces the Seat belongs to (format: {space_id}:{seat_name})
  4. Seat permissions are cached in Redis as a hash for fast lookups
  5. On each API request, the system validates permissions by:
    • Extracting the space ID from the Space request header
    • Finding all matching Seats in the user's JWT hash_perm array 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 (*)

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

warning

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 Seat
  • name: Seat name (must be unique - used to match OIDC memberOf claims)
  • 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 Seat
  • created_at: Timestamp when the Seat was created
  • updated_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 identifier
  • memberOf: Array of Seat names the user belongs to (from OIDC provider)
  • exp: Token expiration timestamp
  • iat: 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 OIDC memberOf claims)
  • 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:

  1. Extract spaceId from Space header: "space-123"
  2. Search hash_perm array: Find "space-123:TrainingDeveloper"
  3. Extract seat_name: Split entry → "TrainingDeveloper"
  4. Query Redis hash seat:TrainingDeveloper:
    • Check field trainings:list → Not found ❌
    • Check field * (wildcard) → Not found ❌
  5. Required permission: trainings:list
  6. 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:

  1. Extract spaceId from Space header: "space-123"
  2. Search hash_perm array: Find "space-123:TrainingDeveloper"
  3. Extract seat_name: Split entry → "TrainingDeveloper"
  4. Query Redis hash seat:TrainingDeveloper:
    • Check field trainings:create → Found ✅
  5. Required permission: trainings:create
  6. Result:200 OK

Request 2: Accessing space-456 (Multiple Seats)

Request:

GET /v1/trainings
Space: space-456
Authorization: Bearer <JWT>

Permission Validation Flow:

  1. Extract spaceId from Space header: "space-456"
  2. Search hash_perm array: Find multiple entries:
    • "space-456:TrainingDeveloper"
    • "space-456:TrainingAdmin"
  3. Extract all seat_names: ["TrainingDeveloper", "TrainingAdmin"]
  4. Query Redis for all seats:
    • seat:TrainingDeveloper → Check field trainings:list → Found ✅
    • seat:TrainingAdmin → Check field * → Found ✅ (wildcard grants all)
  5. Required permission: trainings:list
  6. 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:

  1. Extract spaceId from Space header: "space-999"
  2. Search hash_perm array: No entry starting with "space-999:"
  3. 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:

  1. Extract spaceId from Space header: "space-123"
  2. Search hash_perm array: Find "space-123:TrainingAdmin"
  3. Extract seat_name: Split entry → "TrainingAdmin"
  4. Query Redis hash seat:TrainingAdmin:
    • Check field trainings:delete → Not found, but...
    • Check field * → Found ✅ (wildcard grants all)
  5. Required permission: trainings:delete
  6. 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:

  1. Extract spaceId from Space header: "space-456"
  2. Search hash_perm array: Find "space-456:TrainingDeveloper"
  3. Extract seat_name: Split entry → "TrainingDeveloper"
  4. Query Redis hash seat:TrainingDeveloper:
    • Check field trainings:create → Found ✅
  5. Required permission: trainings:create
  6. Result:200 OK

Note: The same Seat grants the same permissions in all three spaces (space-123, space-456, and space-789).