Multi-Party Login Flow
Overview
This document describes the IAM aspects of multi-party support: the rules governing how accounts are associated with parties, the login flow, and the wire protocol for party selection. For the party hierarchy, RLS policies, and data ownership model see Multi-Party Architecture.
Account-Party Rules
Every account must be associated with at least one party. Zero parties is a misconfiguration and login is rejected.
Account Types and Their Parties
| Account Type | Party Type | Party Count | Notes |
|---|---|---|---|
| Super admin | system |
Exactly 1 | Bound to system party at provisioning |
| Tenant admin | system |
Exactly 1 | Bound to system party at tenant bootstrap |
| User account | operational |
1 or more | Assigned by tenant admin after party creation |
Key constraints:
- The system party cannot be mapped to user accounts. Only admin accounts (super admin, tenant admin) may be bound to it.
- A user account must have at least one operational party. Having zero parties is treated as a misconfiguration and the login is rejected with an explicit error.
- Only the tenant admin can create new parties via the Parties dialog and assign accounts to them via the Account-Parties junction.
Invariants
Super admin ──► system party only (auto-assigned, immutable)
Tenant admin ──► system party only (auto-assigned at bootstrap, immutable)
User account ──► one or more operational parties (assigned by tenant admin)
(zero parties is forbidden; login will be rejected)
Login Flow
After successful password authentication the server queries account_parties
and applies the following rules:
┌──────────────────────┐
│ Login Request │
│ user@hostname │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Resolve tenant │
│ from hostname │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Authenticate user │
│ (password check) │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Query account_parties│
│ for this account │
└──────────┬───────────┘
│
┌─────┴─────┬──────────┬───────────────┐
│ 0 parties │ 1 party │ N parties │
▼ ▼ ▼ │
┌─────────┐ ┌─────────┐ ┌──────────────┐ │
│ Reject │ │ Auto- │ │ Return party │ │
│ login │ │ select │ │ list; do not │ │
│ (error) │ └────┬────┘ │ bind session │ │
└─────────┘ │ └──────┬───────┘ │
│ │ │
│ ┌────▼─────────┐ │
│ │ Client shows │ │
│ │ party picker │ │
│ └────┬─────────┘ │
│ │ │
└─────────────┘ │
│ │
▼ │
┌──────────────────────┐
│ Client sends │
│ select_party_request │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Server validates │
│ party belongs to │
│ account │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Compute visible │
│ party set via │
│ recursive SQL fn │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Bind party to │
│ session │
└──────────────────────┘
0 Parties — Login Rejected
If account_parties returns zero rows the login is rejected immediately. The
client receives a login_response with success = false and the error message:
Account has no party assignment. Please contact your administrator.
The server logs a WARN for each such event. This condition indicates a database misconfiguration and should be investigated by the tenant admin.
1 Party — Auto-Select
The single party is selected automatically without user interaction. The
login_response carries:
selected_party_id— the UUID of the auto-selected party.available_parties— empty (no picker needed).
The session is fully bound before the response is sent.
N Parties — Client-Side Picker
The server fetches the display name of each party from
ores_refdata_parties_tbl and returns them in login_response.available_parties.
The session is not bound yet; selected_party_id is nil. The client shows a
PartyPickerDialog and, upon confirmation, sends a select_party_request.
Wire Protocol
login_response Extensions
Two fields were appended to the existing login_response (protocol v38):
| Field | Type | Description |
|---|---|---|
selected_party_id |
UUID (16 bytes) | Non-nil when party was auto-selected (1 party) |
available_parties |
vector<party_summary> |
Non-empty when client must show picker (N>1) |
party_summary is a value type serialised inline:
| Field | Wire Format |
|---|---|
id |
16 bytes (UUID) |
name |
uint16 length + UTF-8 bytes |
select_party_request (0x2046)
Sent by the client after the user selects a party from the picker.
| Field | Type | Description |
|---|---|---|
party_id |
UUID (16 bytes) | The chosen party UUID |
select_party_response (0x2047)
Server confirmation of party binding.
| Field | Type | Description |
|---|---|---|
success |
bool | true when party was bound |
error_message |
string | Non-empty on failure |
Server Validation for select_party_request
- Verify the session is authenticated.
- Re-query
account_partiesfor the session's account. - Verify the requested
party_idis in that list; reject if not. - Compute
visible_party_idsviaores_refdata_visible_party_ids_fn. - Call
auth_session_service::update_session_party()to bind the session. - Return
select_party_response{.success = true}.
Error Cases
| Scenario | Outcome |
|---|---|
| 0 parties assigned to account | login_response{success=false, error"…"}= |
| N-party picker cancelled by user | Client disconnects; login form re-enabled |
party_id not in account's party list |
select_party_response{success=false} |
| Party lookup fails (DB error) | Session continues without party context (degraded mode) |
Protocol Version
The login_response extension is a breaking wire-format change. The protocol
major version was bumped from 37 to 38 at the same time as this feature was
introduced. Clients and servers with mismatched major versions will fail the
handshake before login is attempted.