Skip to main content

Access Control

This page documents how authentication and authorization work in the Boards GraphQL API.

Overview

The Boards API uses a layered security model:

  1. Authentication - Verifying user identity via JWT tokens
  2. Authorization - Checking permissions for specific operations
  3. Multi-tenancy - Isolating data between tenants

Authentication

Request Headers

Include authentication credentials in HTTP headers:

POST /graphql HTTP/1.1
Authorization: Bearer <jwt-token>
X-Tenant: <tenant-id>
Content-Type: application/json
HeaderRequiredDescription
AuthorizationYes*JWT bearer token
X-TenantDependsTenant identifier (for multi-tenant deployments)

*Required for authenticated operations. Public queries work without authentication.

JWT Token Format

The API expects standard JWT tokens with these claims:

{
"sub": "user-unique-id",
"email": "user@example.com",
"iat": 1704067200,
"exp": 1704153600
}
ClaimDescription
subSubject - unique user identifier from auth provider
emailUser's email address
iatIssued at timestamp
expExpiration timestamp

Auth Providers

Boards supports multiple authentication providers:

ProviderConfigurationDescription
noneBOARDS_AUTH_PROVIDER=noneNo authentication (development only)
jwtBOARDS_AUTH_PROVIDER=jwtGeneric JWT validation
supabaseBOARDS_AUTH_PROVIDER=supabaseSupabase Auth integration

See Auth Providers for detailed configuration.


Authorization

Permission Model

Authorization is based on user relationships to resources:

User
├── Owns Boards (owner_id)
└── Member of Boards (board_members)
└── Has Role (VIEWER, EDITOR, ADMIN)

Board Access Rules

Access to boards is determined by:

  1. Public boards - Accessible to everyone (read-only without auth)
  2. Owner - Full access to all operations
  3. Admin member - Can manage members and edit
  4. Editor member - Can create generations
  5. Viewer member - Read-only access
def can_access_board(board, auth_context):
# Public boards are accessible to everyone
if board.is_public:
return True

# Private boards require authentication
if not auth_context.is_authenticated:
return False

# Owner has access
if board.owner_id == auth_context.user_id:
return True

# Check membership
return any(
member.user_id == auth_context.user_id
for member in board.board_members
)

Query Authorization

QueryAuth RequiredAccess Rule
meYesReturns authenticated user
userYesAny authenticated user
boardDependsPublic or user has access
myBoardsYesOwned or member boards
publicBoardsNoAll public boards
searchBoardsYesAccessible boards only
generationDependsBoard access required
recentGenerationsYesAccessible boards only
generatorsNoPublic information

Mutation Authorization

MutationRequired Role
createBoardAuthenticated
updateBoardOwner or Admin
deleteBoardOwner only
addBoardMemberOwner or Admin
removeBoardMemberOwner or Admin
updateBoardMemberRoleOwner or Admin
createGenerationOwner, Admin, or Editor
cancelGenerationCreator, Owner, or Admin
deleteGenerationCreator, Owner, or Admin
regenerateOwner, Admin, or Editor
uploadArtifactOwner, Admin, or Editor

Board Roles

BoardRole Enum

enum BoardRole {
VIEWER
EDITOR
ADMIN
}

Role Permissions

PermissionViewerEditorAdminOwner
View boardYesYesYesYes
View generationsYesYesYesYes
Create generationsNoYesYesYes
Delete own generationsNoYesYesYes
Delete any generationNoNoYesYes
Add membersNoNoYesYes
Remove membersNoNoYesYes
Update member rolesNoNoYesYes
Update board settingsNoNoYesYes
Delete boardNoNoNoYes

Query Filters

BoardQueryRole

Filter boards by your relationship:

enum BoardQueryRole {
ANY # All accessible boards
OWNER # Only boards you own
MEMBER # Only boards where you're a member (not owner)
}

Example

query GetOwnedBoards {
myBoards(role: OWNER) {
id
title
}
}

query GetSharedBoards {
myBoards(role: MEMBER) {
id
title
owner {
displayName
}
}
}

SortOrder

Sort results by creation or update time:

enum SortOrder {
CREATED_ASC # Oldest first
CREATED_DESC # Newest first
UPDATED_ASC # Least recently updated
UPDATED_DESC # Most recently updated
}

Multi-Tenancy

Tenant Isolation

In multi-tenant deployments, all data is scoped to a tenant:

  • Users belong to a tenant
  • Boards belong to a tenant
  • Generations belong to a tenant

Cross-tenant access is never permitted.

Specifying Tenant

Include the X-Tenant header:

X-Tenant: my-tenant-id

Or use a subdomain (if configured):

https://my-tenant.boards.example.com/graphql

See Multi-Tenancy for detailed configuration.


Error Responses

Authentication Errors

{
"errors": [
{
"message": "Not authenticated",
"path": ["myBoards"],
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}

Authorization Errors

{
"errors": [
{
"message": "You don't have permission to access this board",
"path": ["board"],
"extensions": {
"code": "FORBIDDEN"
}
}
]
}

Invalid Token

{
"errors": [
{
"message": "Invalid or expired token",
"path": ["me"],
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}

Security Best Practices

Client-Side

  1. Store tokens securely - Use httpOnly cookies or secure storage
  2. Handle expiration - Refresh tokens before they expire
  3. Validate responses - Check for auth errors and redirect to login

Server-Side

  1. Validate all inputs - Never trust client data
  2. Use parameterized queries - Prevent injection attacks
  3. Log access attempts - Audit sensitive operations
  4. Rate limit - Prevent brute force attacks

Implementation Details

Access control logic is implemented in:

  • packages/backend/src/boards/graphql/access_control.py - Core authorization functions
  • packages/backend/src/boards/auth/ - Authentication providers
  • packages/backend/src/boards/auth/middleware.py - Request middleware

Key Functions

# Get auth context from GraphQL info
async def get_auth_context_from_info(info: strawberry.Info) -> AuthContext | None

# Check if user can access a board
def can_access_board(board: Boards, auth_context: AuthContext | None) -> bool

# Check if user is owner or member
def is_board_owner_or_member(board: Boards, auth_context: AuthContext | None) -> bool