Saltar al contenido principal

Multi-tenancy

How AlphaSwarm turns a Microsoft Entra ID tid claim into an Organization → Team → User → Membership chain — and what keeps a B2B guest from another tenant from leaking into the wrong org.

Identity flow​

Schema​

TablePurpose
organizationsTop of the AlphaSwarm tenancy tree (multi-tenant)
teamsSubgroup within an org
workspacesVisibility-scoped container of projects + labs
projects / labsThe user-facing buckets where strategies / RAG corpora live
usersAuthenticated identities (one row per Entra oid)
membershipsPolymorphic (user, scope_kind, scope_id, role) grants
entra_tenant_linksMulti-tenant Entra tid → AlphaSwarm organization_id index (NEW)
broker_credentialsMulti-tenant BYOK credentials (envelope-encrypted)

Schema migrations:

  • 0017_tenancy_foundation.py — original default-* seed.
  • 0050_terraform_iac_plus_entra.py — adds entra_tenant_links + the Terraform tables.
  • 0051_seed_wiley_tech.py — seeds the canonical "Wiley Tech" org + user "Julian" + transfers every legacy default-*-owned row.

Runtime Context & Scoped Operations​

AlphaSwarm uses alphaswarm.tenancy.runtime_context.get_runtime_context() as the single source of truth for the active tenant during a request or task. This context carries the tenant_id, org_id, and project_id.

Scoped Data Access​

Database models utilize ProjectScopedMixin (and OrganizationScopedMixin) to enforce tenant isolation at the ORM level. The alphaswarm.persistence.db.get_session helper automatically applies tenant filters to queries when a runtime_context is present.

Multi-Tenant Trading (BYOK)​

The Account Management System (AMS) uses the BrokerCredentialStore to manage per-tenant brokerage credentials. Credentials are envelope-encrypted using a master key (or AWS KMS/Vault) and stored in the broker_credentials table. The build_brokerage factory resolves these credentials based on the active tenant_id.

Statuses (see :data:ENTRA_TENANT_STATUSES):

StatusBehaviour
pendingCreated by first-login of an unknown tid. User signs in but lands on an "awaiting org admin" surface (no Memberships granted).
activeNew logins from the tenant auto-provision into the linked org + workspaces.
suspendedSign-ins from the tenant still resolve, but no new Memberships are granted.
revokedSign-ins from the tenant are blocked at provision time.

AGENTS rule 44: organization provisioning from Entra ID claims goes through EntraTenantLink. Don't auto-create org rows from raw tid claims. The data.tenancy.link_org_to_entra_tenant MCP tool (REST: POST /tenancy/entra-links) is the only sanctioned ingress for creating (and optionally immediately activating) a link. Links auto-created as pending by a first sign-in from an unrecognized tenant are instead promoted via the frontend EntraTenantLinkWizard, a 3-step wizard that calls POST /tenancy/entra-links/{link_id}/promote.

The alphaswarm_client SPA's AuthProvider has standardised on Microsoft Entra ID via MSAL (ADR-013): the "Microsoft" login button (loginWithMicrosoft) is a direct MSAL redirect, not an Auth0-federated one — alphaswarm_client no longer ships an Auth0 SDK/provider at all. The backend's Auth0Provider and the Terraform-provisioned connection=azure-ad-myorg Enterprise Connection (see scim-provisioning.md) still exist for non-SPA / legacy consumers, but the current customer-facing SPA does not route Microsoft sign-in through them, so _apply_entra_tenant_link (which is a no-op unless claims_provider(claims) == "msal_entra", i.e. the token's iss is login.microsoftonline.com) is not reached via that path today.

MsalEntraProvider remains registered through IdentityProviderMeta and activates when ALPHASWARM_AUTH_PROVIDER=msal_entra — this is the path the SPA's direct-MSAL login and provision_user_from_claims actually exercise. Super-admin promotion of the resulting pending EntraTenantLink rows is managed in alphaswarm_client/src/components/onboarding/EntraTenantLinkWizard.tsx.

App role mapping​

Entra ships app roles in a top-level roles claim array (e.g. ["alphaswarm.admin", "alphaswarm.terraform.operator"]). The provisioning logic maps them onto the AlphaSwarm role lattice (viewer < editor < admin < owner):

# alphaswarm/auth/user.py::_apply_entra_tenant_link
# Multi-word roles fold to the tail token:
# alphaswarm.terraform.operator -> "operator" -> editor
# alphaswarm.terraform.approver -> "approver" -> admin

Per-link overrides live in EntraTenantLink.role_mapping (JSON), keyed by the raw Entra app-role string with the target AlphaSwarm role as the value. The seeded Wiley Tech link (alembic/versions/0051_seed_wiley_tech.py::_seed_entra_tenant_link) actually seeds an empty role_mapping ({}), so its logins fall through to the _fold_entra_role_to_tenancy defaults above rather than a custom per-link mapping; an admin can populate an override like this via the onboarding wizard or the POST /tenancy/entra-links / POST /tenancy/entra-links/{link_id}/promote APIs:

{
"alphaswarm.admin": "owner",
"alphaswarm.editor": "editor",
"alphaswarm.viewer": "viewer",
"alphaswarm.terraform.operator": "editor",
"alphaswarm.terraform.approver": "admin"
}

Onboarding wizards (frontend)​

/admin/onboarding hosts three wizards behind tabs:

  1. OrgCreateWizard (4 steps) — name / billing / default structure / review. Seeds the canonical Core team + Main workspace + Main project + Main lab (from configs/tenants/tenant_default_template.yaml).
  2. EntraTenantLinkWizard (3 steps) — promotes an already-pending EntraTenantLink (auto-created on first sign-in from an unrecognized Entra tenant): (1) pick the pending link, (2) choose the org + bootstrap default role, (3) confirm promotion (POST /tenancy/entra-links/{link_id}/promote). It does not take a manually-typed tid — creating a link ahead of time with a primary domain / allowed email domains / full app-role mapping is done via the POST /tenancy/entra-links API instead (see msal-entra-setup.md).
  3. UserInviteWizard (3 steps) — email + display name / scope + role / review + send (Entra B2B invitation when MSAL is configured).

Tenant template files​

configs/tenants/ hosts three YAMLs:

  • tenant_default_template.yaml — default org structure created on data.tenancy.create_organization.
  • roles_default_template.yaml — canonical app-role → AlphaSwarm-role mapping.
  • user_invite_template.yaml — Entra B2B invite email body + custom claims payload.

Seeded state​

After running alembic upgrade head against a fresh DB:

SlugTypeNotes
defaultOrganizationLegacy 0017 seed (preserved for FK chains)
wiley-techOrganizationNew canonical seed (Wiley Tech)
coreTeamDefault team under wiley-tech
mainWorkspaceDefault workspace under wiley-tech
mainProjectDefault project under main workspace
mainLabDefault lab under main workspace
[email protected]UserOwner on every Wiley Tech scope

Every legacy *_runs / bots / agent_runs_v2 / analysis_runs / ... row that previously pointed at default-org / default-user is re-stamped to point at wiley-tech / [email protected] (see _restamp_legacy_rows in alembic/versions/0051_seed_wiley_tech.py). The legacy default-* rows stay in place so any orphan FK still resolves.