Repository navigation
feat(api,relay): cloud workspaces reach hosts their owner opened - #9408
Conversation
Whether a host's owner lets their cloud workspaces reach it through the relay. Defaults to false, so every existing host stays closed. A constant default on Postgres 11+ is metadata-only: no table rewrite.
A cloud workspace holds no user token, so `superset hosts list` from a box showed every host offline and `--host` calls failed as "Session expired". - host.list asks the relay for presence with a short-lived server token when the caller has no bearer (a box, or an API key). - host.setCloudWorkspaceAccess: owner-only switch for v2_hosts.allow_cloud_workspaces. Not on the sandbox allow-list, so a box cannot open a host to itself. - host.cloudWorkspaceAccess: the relay's per-call check for a box's credential: the creator has access to the host and the host is open. - The sandbox firewall adds the workspace credential on requests to the relay host too, so nothing enters the box. - host.list and host.roster report allowCloudWorkspaces (additive).
With no bearer and the firewall-added workspace credential, the relay asks the API on every call whether the box may reach this host, and refuses with a message that says so when the host is closed. A box gets HTTP only: the firewall refuses WebSocket upgrades on hosts it rewrites, and a stream would outlive the per-call check. The credential never reaches the host.
|
Caution Review failedThe pull request is closed.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
1 issue found across 34 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/relay/src/access.ts">
<violation number="1" location="apps/relay/src/access.ts:132">
P2: An API outage is converted to 401 for host tRPC routes. Preserve the 500 result as `INTERNAL_SERVER_ERROR`, otherwise cloud-workspace callers refresh credentials instead of retrying the transient access-check failure.</violation>
</file>
Architecture diagram
sequenceDiagram
participant WS as Cloud Workspace
participant FW as Vercel Firewall
participant R as Relay
participant API as API (tRPC)
participant DB as Database
participant H as Host Service
Note over WS,H: Runtime: Cloud Workspace → Host via Relay
WS->>FW: HTTP request to relay (no bearer token)
FW->>FW: Add x-superset-sandbox-credential header
WS->>R: Forward request with credential header
R->>R: authenticateCloudWorkspace(credential, hostId)
R->>API: host.cloudWorkspaceAccess.query({hostId}) [creates sandbox client]
API->>DB: Check v2UsersHosts for (creator, host) membership
API->>DB: Check v2Hosts.allowCloudWorkspaces column
DB-->>API: Access decision
API-->>R: Return {allowed, userId, workspaceId} or denial
alt Credential valid AND host allows cloud workspaces
R->>R: Build auth context from API response
R->>H: Forward request (strip credential header, add user id)
H-->>R: HTTP response
R-->>WS: 200 OK + response body
else Credential valid but host closed
API-->>R: allowed=false, reason="host_closed"
R-->>WS: 403 "host does not accept cloud workspaces"
else Forged/invalid credential
API-->>R: 401 UNAUTHORIZED
R-->>WS: 401 Unauthorized
end
Note over WS,R: WebSocket Upgrade from Box
WS->>R: WebSocket upgrade request with credential header
R->>R: Detect cloudWorkspaceId + WS upgrade
R-->>WS: 403 Forbidden (HTTP only)
Note over WS,API: host.list presence fallback
WS->>API: host.list (no bearer token)
API->>API: Mint 60s "automation-presence" JWT
API->>R: fetchRelayPresence with presence token
R-->>API: Online/offline status per host
API-->>WS: Host list with allowCloudWorkspaces + presence
Note over WS,API: Owner controls access
Owner->>API: host.setCloudWorkspaceAccess({allowed: true})
API->>API: Check caller is host owner (not sandbox)
API->>DB: Update allowCloudWorkspaces
DB-->>API: Success
API-->>Owner: {allowed: true}
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| const code = (error as { data?: { code?: string } }).data?.code; | ||
| return { | ||
| ok: false, | ||
| reason: code === "UNAUTHORIZED" ? "unauthorized" : "error", |
There was a problem hiding this comment.
P2: An API outage is converted to 401 for host tRPC routes. Preserve the 500 result as INTERNAL_SERVER_ERROR, otherwise cloud-workspace callers refresh credentials instead of retrying the transient access-check failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/relay/src/access.ts, line 132:
<comment>An API outage is converted to 401 for host tRPC routes. Preserve the 500 result as `INTERNAL_SERVER_ERROR`, otherwise cloud-workspace callers refresh credentials instead of retrying the transient access-check failure.</comment>
<file context>
@@ -87,3 +91,45 @@ export async function checkHostAccessForRegister(
+ const code = (error as { data?: { code?: string } }).data?.code;
+ return {
+ ok: false,
+ reason: code === "UNAUTHORIZED" ? "unauthorized" : "error",
+ };
+ }
</file context>
There was a problem hiding this comment.
Fixed in a65d1b4. A failed access check (status 500) now maps to INTERNAL_SERVER_ERROR on the host tRPC routes, not UNAUTHORIZED, with a test that simulates the API being down.
|
Review: needs changes. The design is sound, and the relay behaves as described when run locally. But CI Typecheck fails because of this PR, and the opt-in reaches further than its docs say.
Relay checked locally with Full report: https://app.superset.sh/page/pr-9408-review-cloud-workspaces-reach-opted-in-hos-zm0w3q |
host.roster rows now carry allowCloudWorkspaces, and the row mobile builds for a cloud workspace's own sandbox has to match. A sandbox is not a host its owner opened, so false.
host.cloudWorkspaceAccess matched any v2_users_hosts row, so once an owner opened a host, the boxes of every member they had added could drive it too. The decision is now a pure function that requires the owner role, with tests for owner, member, closed, cross-org and non-box callers. Also use the catalog's existing German term for host owner.
…ges retryable - A box's WebSocket upgrade and a credential that is not the shape the API mints are refused without an API round trip. - A failed access check (API unreachable) is INTERNAL_SERVER_ERROR on the host tRPC routes, not UNAUTHORIZED, so callers retry instead of treating it as an auth failure.
There was a problem hiding this comment.
All reported issues were addressed across 8 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
|
Addressed:
|
|
Preview update published for development build |
Stack: 2 of 3 — #9407 (DB) → this PR → desktop + CLI. Merge after #9407.
Summary
superset hosts listfrom a cloud workspace now shows real online state instead of every host offline.v2_hosts.allow_cloud_workspaces). Otherwise it gets a clear 403 instead of "Session expired".Why / Context
Reported by lucas@traceroot.ai: from a cloud workspace, his online Mac showed offline and
--hostcalls said "Session expired" whileauth whoamiworked. The cause was the relay, not the API or host-service. A box holds no user token, sohost.listnever asked the relay for presence, and the relay answered every--hostcall with 401, which the CLI shows as "Session expired". Also asked for in SUPER-2661 and SUPER-2852.How It Works
host.listfalls back to a 60 s server presence token (the same pattern as automation dispatch) when the caller has no bearer. This also fixes presence for API-key callers.deriveSandboxCredentialsadds the workspace credential header for the relay host, the same value the API already gets.host.cloudWorkspaceAccesswith the header, on every request and uncached. The API answers yes only if the box's creator owns the host and the column is true. A host member's boxes are refused, even when the host is open. The relay then removes the credential and forwards. WebSocket upgrades from a box, and credentials that aren't the shape the API mints, are refused before any API call. The firewall can't rewrite WebSocket upgrades anyway, and a stream would outlive the per-call check. If the API is unreachable, the relay returnsINTERNAL_SERVER_ERROR(retryable), notUNAUTHORIZED.host.setCloudWorkspaceAccessis owner-only and not onSANDBOX_ALLOWED_PROCEDURES(a test enforces this), so a box can't open a host to itself.host.listandhost.rosterreportallowCloudWorkspaces.Security
sessionFromOAuthBeareraccepts any API-signed JWT as a full user session regardless ofscope, which includes the automation tokens minted for hosts. Worth its own ticket.Manual QA (dev stack: local API + relay + dev desktop)
host.listas the box: the dev host showsonline: truehost.setCloudWorkspaceAccess: 403Testing
bun testinpackages/shared(1547),packages/trpc(873),apps/relay(19),packages/i18n(169), all passingdecideCloudWorkspaceAccessis a pure function with tests for owner (open/closed), member, no access, cross-org and non-box callersbun run typecheckin the same four packagesbun run check:i18n(one new server error, translated in all locales)Compatibility
Additive only: two new procedures and new output fields on
host.list/host.roster. No input or error-code changes for released clients (checked againstpackages/trpc/AGENTS.md).Deployment / Rollout
Rollback
Revert this PR. The relay then refuses box callers again (401), and the column goes unread.
Summary by cubic
Cloud workspaces can now reach hosts their owner opened, instead of showing every host offline and failing
--hostcalls with "Session expired."The relay refused a box's calls because a box holds no user token. It now recognizes the firewall-added workspace credential, asks
host.cloudWorkspaceAccesson every call, and forwards only if the host's owner created the box and enabledallowCloudWorkspaces; a member's box is refused even for an open host. The credential never reaches the host, and a malformed credential or a WebSocket upgrade is refused without asking the API. If the API is unreachable during the check, the call fails with a server error so callers retry instead of treating it as an auth failure.host.listmints a 60-second server presence token when the caller has no bearer, which also fixes presence for API-key callers.host.listandhost.rosterreportallowCloudWorkspaces; the mobile sandbox host row sets it false.host.setCloudWorkspaceAccessis owner-only and not on the sandbox allow-list.Security
Written for commit 680819f. Summary will update on new commits.