Repository navigation
fix(auth): scope alerts/reports to readable explorations; tighten datasource writes - #92
Merged
Merged
Conversation
…asource writes Follow-up to #91 (independent review): - alerts/reports insert+update checks were always true (user_id preset to the caller) and exploration_id was unconstrained. The alert/report runs the exploration as its owner, so anyone knowing an exploration id could schedule another tenant's query to their own webhook/email. The row's exploration must now be one the caller can read, and team_id must be null or a team the caller belongs to. - datasources update/delete: the creator branch now only covers personal rows (team_id null), so a row planted in another team cannot be kept under control by its creator. - create_team: admin-secret callers (role admin) are trusted, so the StepCI workflows create teams with the admin secret and keep x-hasura-user-id for ownership. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… creator branches Review of #92: - send_test_alert loaded the exploration with the admin secret and ran it as its owner, delivering any tenant's results to a caller-chosen webhook/Slack/email. It now reads the exploration with the caller's token and refuses when there is none. - The "caller created the datasource" branch in the alerts/reports checks and the explorations select filter now requires team_id null, so a user removed from a team can no longer read or schedule its explorations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The action read branches_by_pk and the latest version's dataschema code with the admin secret and no membership check, then returned a 7-day presigned zip: anyone with a foreign branch id could download that tenant's models. The action now forwards client headers and the handler reads with the caller's token, refusing when there is none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 free
to 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.
Follow-up to #91, from an independent review (CodeRabbit doesn't run on this repo).
1. alerts / reports: cross-tenant query exfiltration (High)
The insert and update checks were
_or: [user_id = caller, team member].user_idis preset to the caller, so the check was always true, andexploration_idwas unconstrained.checkAlert(and the report runner) executes the exploration as its owner (exploration.user_id). Anyone who knew another tenant's exploration id could schedule an alert or report that runs that tenant's query and delivers the results to their own webhook or email.Fix (insert and update
check):explorationsselect (datasource owned by the caller, or in a team the caller belongs to), andteam_idis null or a team the caller belongs to.Select and delete filters are unchanged.
2. datasources: the creator branch only covers personal rows
Update and delete filters (and the update check) allowed
user_id = calleron any row. A datasource planted in another team before #91 stayed under its creator's control, e.g. repointingdb_params. That branch now requiresteam_idnull. Team rows are governed by membership.3.
create_team: admin-secret callers are trusted#91 limited user-invoked
create_teamto portal admins, which broke the StepCI workflows that create teams with a fresh@example.comuser. Roleadmin(admin secret) is now allowed;x-hasura-rolecan't be spoofed, since user JWTs don't includeadmin. The 7 workflows'create_teamsteps now send the admin secret and keepx-hasura-user-id, so the test user still becomes owner.access-controls/provisioning.ymlalready used the admin secret.Not changed (noted)
admincan setmember_rolestoowner(pre-existing): an admin can promote themselves and then invite.create_team(provision/route.ts,[serviceId]/team/route.ts) appear dead already (they depend on/auth/register).user_idis not a member of theirteam_id(planted rows).Tests
node --testinservices/actions: 64/64, including newcanCreateTeamcases (admin role allowed, user role refused).tables.yamland all edited workflows parse; the permission JSON renders as described above.4.
send_test_alertbypass (High), added after reviewrpc/sendTestAlert.jsloaded the exploration with the admin secret and ran it as its owner (sendExplorationScreenshot), delivering results to a caller-chosen webhook, Slack or email. That made the alerts/reports check above moot. It now reads the exploration with the caller's token, so theexplorationsselect rule applies, and refuses when there is no token (fetchGraphQLwould otherwise fall back to the admin secret).5. Former team members (Medium), added after review
The "caller created the datasource" branch now requires
team_idnull in the alerts/reports checks and in theexplorationsselect filter. A user removed from a team can no longer read or schedule that team's explorations.Known, not changed here
user_idcannot read, and delete them (the delete trigger removes the cron job).Tests: 65/65 (adds a
send_test_alertno-token refusal case).6.
export_data_models(High), found by an admin-secret sweepThe action read
branches_by_pkand the latest version's dataschemacodewith the admin secret and no membership check, then returned a 7-day presigned zip. Anyone with a foreign branch id could download that tenant's full models. The action now hasforward_client_headers: trueand the handler reads with the caller's token, refusing when there is none. Members without model read rights now get an empty export, matching thedataschemasselect permission.Follow-ups from the sweep (Low, not in this PR)
profileTable.js:205doesn't check the bodybranchId(leaks a foreign table's model existence andgeneration_filters). Fix:authorizeBranchAccess, as generate/smart-generate do.validateInBranch.jsappend mode leaks branch existence. Same fix.fetchDataset/genSqlfall back to the admin secret without a token. Only reachable by calling actions/rpcdirectly, which has no auth of its own.Tests: 66/66.
🤖 Generated with Claude Code