Back to Bytebase

Step 04: Background Runners, Auth Flow, and Member Management

docs/plans/saas/04.runners-and-auth.md

3.17.16.1 KB
Original Source

Step 04: Background Runners, Auth Flow, and Member Management


1. Background Runners

Runners poll for pending tasks across ALL workspaces with a single query (no workspace filter). They resolve workspace context from the task's parent chain when processing.

Runner-Specific Workspace Resolution

RunnerWorkspace resolutionNotes
taskrun/task_run → task → plan → project → workspaceMain execution pipeline
plancheck/plan_check_run → plan → project → workspacePlan validation
approval/issue → project → workspaceApproval workflows
schemasync/instance → workspace (direct)Instance is a root table
mail/Varies by notification sourceResolve from the entity being notified about

Cross-Workspace Store Methods

Located in backend/store/runner_queries.go:

  • GetProjectByResourceID — by globally unique PK, no workspace filter
  • GetInstanceByResourceID — by globally unique PK, no workspace filter
  • ListAllInstances — returns all instances across workspaces
  • DeleteExpiredExportArchivesAll — cleanup across all workspaces

2. Auth Flow

Login Flow

  1. User authenticates (password/SSO/MFA) → global principal table
  2. Workspace resolved:
    • Prefers user.Profile.LastLoginWorkspace if still valid
    • Falls back to first workspace from IAM membership
    • Self-hosted includes allUsers workspaces; SaaS requires explicit membership
  3. JWT generated with workspace_id claim
  4. Access token set as HTTP-only cookie (expires 30s after JWT for refresh support)
  5. Refresh token stored in web_refresh_token table

JWT Structure

json
{
  "sub": "[email protected]",
  "workspace_id": "ws-abc123",
  "aud": ["bb.user.access"],
  "iss": "bytebase",
  "iat": 1709654400,
  "exp": 1709740800
}

Token Refresh

  1. Access token cookie outlives JWT by 30 seconds
  2. On 401, frontend calls Refresh() with refresh token cookie
  3. Refresh() extracts workspace from the expired JWT in the access token cookie (signature-verified)
  4. Verifies user still a member of the workspace
  5. Issues new access token + rotates refresh token (preserving absolute session expiry)

Workspace Switching

SwitchWorkspace RPC (POST /v1/auth:switchWorkspace):

  1. Validates user is END_USER (not service account/OAuth2 token)
  2. Verifies membership in target workspace (direct or via group)
  3. Validates workspace sign-in policies (domain restrictions)
  4. Checks MFA if target workspace requires it (with lockout protection)
  5. Consumes old refresh token, preserves its absolute expiry
  6. Issues new JWT + cookies for the target workspace
  7. Frontend reloads via BroadcastChannel (all tabs switch together)

Workspace Discovery Query

Finds workspaces where a user is a member — directly, via group, or via allUsers:

sql
SELECT DISTINCT w.resource_id, w.name
FROM workspace w
JOIN policy p ON p.workspace = w.resource_id
WHERE p.resource_type = 'WORKSPACE'
  AND p.type = 'IAM'
  AND w.deleted = FALSE
  AND EXISTS (
    SELECT 1
    FROM jsonb_array_elements(p.payload->'bindings') AS binding,
         jsonb_array_elements_text(binding->'members') AS member
    WHERE member = 'users/{email}'
       OR (member LIKE 'groups/%' AND EXISTS (
            SELECT 1 FROM user_group ug,
                 jsonb_array_elements(ug.payload->'members') AS gm
            WHERE ug.workspace = w.resource_id
              AND 'groups/' || ug.email = member
              AND gm->>'member' = 'users/{email}'
          ))
       OR member = 'allUsers'  -- self-hosted only
  )

Service Accounts and Workload Identities

Workspace-scoped via their own tables (service_account, workload_identity), each with a workspace column. They authenticate directly within workspace context — no workspace picker needed.


3. Member Management

Design: Google Cloud IAM Model

Following Google Cloud's approach — no invitation/pending state. Members are managed by updating the workspace IAM policy.

ActionHow
Add memberAdd email to workspace IAM binding with a role
Remove memberRemove email from all workspace IAM bindings
Change roleUpdate the member's IAM bindings

If the email doesn't have a Bytebase account yet, the IAM binding still exists. When the user signs up later, Signup finds the workspace via IAM membership and auto-joins.

SaaS vs Self-Hosted

BehaviorSelf-HostedSaaS
Add memberCreateUser (admin creates account) OR add email to IAMAdd email to IAM only (no CreateUser)
Principal creationAdmin can create via APIUsers self-register via Signup only
allUsers bindingAllowed (grants access to all principals)Blocked
Member types in IAMusers/{email}, groups/{email}, allUsersusers/{email}, groups/{email}

Member Resolution (Auth Layer)

When verifying workspace membership (verifyWorkspaceMembership), the auth interceptor checks:

  1. Direct membership: member == 'users/{email}'
  2. Group membership: member == 'groups/{groupEmail}' → expand via GetGroupMembersSnapshot
  3. allUsers (self-hosted only): member == 'allUsers'

Group Membership Expansion

Groups are stored in user_group table with members in payload.members[].member (format: users/{email}).

When a group groups/{groupEmail} appears in an IAM binding:

  • Permission checks (iam/manager.go): expands via GetGroupMembersSnapshot (cached)
  • Workspace discovery (store/workspace.go): expands via SQL JOIN to user_group table
  • Auth verification (api/auth/auth.go): expands via GetGroupMembersSnapshot

Frontend UX

SaaS mode:

  • "Members" page: shows workspace members from IAM policy, allows adding emails with role selection
  • "Users" section: hidden (principals are global, not workspace-managed)
  • "Groups" section: shown (workspace-scoped)

Self-hosted mode:

  • "Members" page: shows members, allows adding existing users or creating new ones
  • "Users" section: shown (admin can manage principals)
  • "Groups" section: shown