Uh oh!
There was an error while loading. Please reload this page.
feat: native organization multi-tenancy (tenant isolation) - #1
Merged
Conversation
Activate the dormant org multi-tenancy for OIDC logins: read a configurable org-id claim (claim_org, plus optional claim_org_name) from the token, ensure the organization exists, and mint an org-scoped JWT via GenerateOrgToken so downstream org-aware queries can scope by Claims.OrgID. Provider-agnostic: claim_org is empty by default (login stays global/ server-level, fully backward compatible); set it per IdP, e.g. Zitadel urn:zitadel:iam:user:resourceowner:id. Roles keep flowing through the existing group_role_map. Orgs referenced by the claim are auto-provisioned on first login (id = IdP org id). This is phase A of native tenant isolation; query-path scoping (phase B) and device-token org binding (phase C) follow.
handleListPeers already scoped the paginated path via ListPeersForOrg, but the RustDesk-client envelope path (?accessible/?pageSize -> handleClientPeersList -> buildRustDeskPeerList -> loadRustDeskPeerByID) called the global ListPeers, leaking every tenant's devices to any org-scoped browser. Thread the caller's org id into loadRustDeskPeerByID and use ListPeersForOrg when set; empty org id keeps the global path (backward compatible). Both callers (peer list + groups) pass getOrgIDFromCtx(r). Phase B (query-path scoping), group 1/5.
CDAP device access was role-only: any org-scoped admin could open a live
desktop/terminal/video/audio/file session to ANY tenant's device, and the
device info/manifest/state routes had no auth wrapper at all. Add a
requireDeviceOrgScope router wrapper (reuses peerOrgScopeCheck) on all nine
/api/cdap/devices/{id}/* routes so an org-scoped caller is 403'd on a device
outside their org. Extract the underlying allow-logic into deviceInOrgScope
(no response written) and reuse it to filter handleCDAPListDevices and
handleCDAPAlerts. Global (no-org) users and super/global admins are unaffected.
Phase B (query-path scoping), group 2/5 — the highest-severity leak.Two parts: - On OIDC login with an org claim, link the server user into org_users (ensureOrgMembership, idempotent + best-effort) so the claim-based membership is reflected in the org member tables, not just the JWT. Login is never blocked by a link failure. - Scope handleListUsers and the RustDesk-client handleClientUsersList to the caller's org members via orgUserScope (fails closed on lookup error). Global and super/global-admin callers see all users unchanged. Phase B (query-path scoping), group 3/5.
Bind an organization to an enrollment token (DeviceToken.org_id, dual-backend
schema + migration) so a per-org token placed in the agent's RustDesk config
lands the device in the right tenant atomically at enrollment — no separate
assign call, no race. On a token-gated register, assignEnrolledDeviceToOrg
upserts org_devices from the token's org. Empty org_id keeps today's global
behavior (backward compatible). Admins/uvit mint per-org tokens via
POST /api/tokens {org_id}.
This is the functional keystone: it is what populates org_devices, which the
Phase-B scoping filters against.
Phase C of native tenant isolation.Audit: connection/file/alarm lists and the combined summary drop records whose host device is outside the caller's org (orgFilterByHost on host_id); totals reflect the filtered set for org-scoped callers. The global ops event feed (/api/audit/events, /api/ws/events) mixes non-device server events, so it is staff-only — org-scoped users get an empty feed / 403. Chat: contacts list is scoped to the org's devices (ListPeersForOrg) and the org's operators (orgUserScope); history, send and unread are gated by chatTargetAllowed, which blocks only conversations/targets that are a device in another org (group and operator ids pass, so those chats keep working). Global and super/global-admin callers are unaffected throughout. Phase B (query-path scoping), groups 4-5 of 5 — phase B complete.
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Native organization multi-tenancy (tenant isolation)
Activates and completes BetterDesk's existing but dormant org overlay so a
single instance can safely serve multiple isolated tenants. Users are scoped by
an OIDC org claim; devices by a per-org enrollment token; every
authenticated read path enforces the org boundary.
Fully backward compatible: every change no-ops when there is no org context
(empty claim / empty token org). Existing single-tenant deployments behave
exactly as before.
What each commit does
claim_org, e.g. Zitadelurn:zitadel:iam:user:resourceowner:id),ensureOrg, mint an org-scoped JWT(
GenerateOrgToken). Roles keep flowing through the existinggroup_role_map.org_userson login;scope the user list to the org's members.
requireDeviceOrgScopewrapper on all 9/api/cdap/devices/{id}/*routes; filter device list & alerts. (Closed theworst leak: live remote control of any tenant's device.)
(
?accessible/?pageSize) that bypassed the already-scoped admin path.DeviceToken.org_id(dual-backend schema + migration);a token-gated register auto-assigns the device to the token's org. This
populates
org_devices, which every scope check filters against.global ops event feed is staff-only; scope chat contacts and gate
history/send/unread (group & operator chats still work).
Config to enable (per instance, all existing keys except
claim_org)Mint one per-org enrollment token via
POST /api/tokens {"org_id": "<org>"},place it in the agent's config, run with
ENROLLMENT_MODE=locked.Verification
go build/go vet/go testgreen acrossdb,api,auth. AddedTestJWTOrgTokenCarriesOrgIDandTestDeviceTokenCarriesOrgIDfor the twosecurity-carrying data-path invariants.
Design notes:
.claude/docs/uvit/features/BERYLL_TENANT_ISOLATION.md(workspace).