diff --git a/services/actions/src/rpc/__tests__/teamAuthz.test.js b/services/actions/src/rpc/__tests__/teamAuthz.test.js index c406fe1f..65ad217f 100644 --- a/services/actions/src/rpc/__tests__/teamAuthz.test.js +++ b/services/actions/src/rpc/__tests__/teamAuthz.test.js @@ -59,7 +59,55 @@ describe("create_team authorization", () => { assert.equal(await canCreateTeam({ "x-hasura-user-id": OWNER }, admin), true); }); + it("allows an admin-secret caller (role admin)", async () => { + assert.equal( + await canCreateTeam( + { "x-hasura-role": "admin", "x-hasura-user-id": OUTSIDER }, + notAdmin + ), + true + ); + }); + + it("does not trust a user-role caller that is not a portal admin", async () => { + assert.equal( + await canCreateTeam( + { "x-hasura-role": "user", "x-hasura-user-id": OUTSIDER }, + notAdmin + ), + false + ); + }); + it("allows the users-insert event trigger (no session)", async () => { assert.equal(await canCreateTeam(undefined, notAdmin), true); }); }); + +describe("send_test_alert authorization", () => { + it("refuses without a caller token (never reads with the admin secret)", async () => { + const { default: sendTestAlert } = await import("../sendTestAlert.js"); + const res = await sendTestAlert( + { "x-hasura-user-id": OUTSIDER }, + { + explorationId: "44444444-4444-4444-8444-444444444444", + deliveryType: "WEBHOOK", + deliveryConfig: { url: "https://attacker.example/hook" }, + }, + {} + ); + assert.equal(res.error, true); + }); +}); + +describe("export_data_models authorization", () => { + it("refuses without a caller token (never reads with the admin secret)", async () => { + const { default: exportDataModels } = await import("../exportDataModels.js"); + const res = await exportDataModels( + { "x-hasura-user-id": OUTSIDER }, + { branch_id: "55555555-5555-4555-8555-555555555555" }, + {} + ); + assert.equal(res.error, true); + }); +}); diff --git a/services/actions/src/rpc/createTeam.js b/services/actions/src/rpc/createTeam.js index 58616f20..f9ff99c5 100644 --- a/services/actions/src/rpc/createTeam.js +++ b/services/actions/src/rpc/createTeam.js @@ -42,13 +42,15 @@ const createTeam = async ({ userId, name }) => { /** * The `create_team` action makes the caller OWNER of a team with the default * datasources, so only portal admins may call it. The users-insert event - * trigger arrives without session variables and is unaffected. + * trigger arrives without session variables, and admin-secret callers + * (role `admin`) are already fully trusted; both are unaffected. */ export const canCreateTeam = async ( session, { portalAdmin = isPortalAdmin } = {} ) => { if (!session) return true; + if (session["x-hasura-role"] === "admin") return true; return portalAdmin(session["x-hasura-user-id"]); }; diff --git a/services/actions/src/rpc/exportDataModels.js b/services/actions/src/rpc/exportDataModels.js index f5e00c12..82e8d2c5 100644 --- a/services/actions/src/rpc/exportDataModels.js +++ b/services/actions/src/rpc/exportDataModels.js @@ -24,15 +24,24 @@ const fetchSchemasQuery = ` } `; -export default async (session, input) => { +export default async (session, input, headers) => { const { branch_id: branchId } = input; const userId = session?.["x-hasura-user-id"]; + const authToken = headers?.authorization; try { - const schemasResp = await fetchGraphQL(fetchSchemasQuery, { - branch_id: branchId, - }); + // Read as the caller: an admin-secret read exported any tenant's models. + // Without a caller token fetchGraphQL would fall back to the admin secret. + if (!authToken) { + throw new Error(`Branch ${branchId} not found!`); + } + + const schemasResp = await fetchGraphQL( + fetchSchemasQuery, + { branch_id: branchId }, + authToken + ); const branch = schemasResp?.data?.branches_by_pk; const schemas = branch?.versions?.[0]?.dataschemas || []; diff --git a/services/actions/src/rpc/sendTestAlert.js b/services/actions/src/rpc/sendTestAlert.js index fcde8c22..6b42316f 100644 --- a/services/actions/src/rpc/sendTestAlert.js +++ b/services/actions/src/rpc/sendTestAlert.js @@ -14,12 +14,22 @@ const explorationQuery = ` } `; -export default async (_, input) => { +export default async (_, input, headers) => { const { explorationId, name, deliveryConfig, deliveryType } = input || {}; + const authToken = headers?.authorization; - const queryResult = await fetchGraphQL(explorationQuery, { - id: explorationId, - }); + // Read the exploration as the caller: the runner executes it as its owner, + // so an admin-secret read would deliver any tenant's results. Without a + // caller token fetchGraphQL would fall back to the admin secret. + if (!authToken) { + return apiError("Exploration not found"); + } + + const queryResult = await fetchGraphQL( + explorationQuery, + { id: explorationId }, + authToken + ); const exploration = queryResult?.data?.explorations_by_pk; if (!exploration) { diff --git a/services/hasura/metadata/actions.yaml b/services/hasura/metadata/actions.yaml index af7b6bd8..719b1e03 100644 --- a/services/hasura/metadata/actions.yaml +++ b/services/hasura/metadata/actions.yaml @@ -38,6 +38,7 @@ actions: definition: kind: synchronous handler: '{{ACTIONS_URL}}/rpc/export_data_models' + forward_client_headers: true timeout: 60 permissions: - role: user diff --git a/services/hasura/metadata/tables.yaml b/services/hasura/metadata/tables.yaml index 29b64c8c..9fada1e3 100644 --- a/services/hasura/metadata/tables.yaml +++ b/services/hasura/metadata/tables.yaml @@ -204,13 +204,27 @@ - role: user permission: check: - _or: - - user_id: - _eq: X-Hasura-User-Id - - team: - members: - user_id: - _eq: X-Hasura-User-Id + _and: + - exploration: + _or: + - datasource: + _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id + - datasource: + team: + members: + user_id: + _eq: X-Hasura-User-Id + - _or: + - team_id: + _is_null: true + - team: + members: + user_id: + _eq: X-Hasura-User-Id set: user_id: x-hasura-user-id columns: @@ -266,13 +280,27 @@ user_id: _eq: X-Hasura-User-Id check: - _or: - - user_id: - _eq: X-Hasura-User-Id - - team: - members: - user_id: - _eq: X-Hasura-User-Id + _and: + - exploration: + _or: + - datasource: + _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id + - datasource: + team: + members: + user_id: + _eq: X-Hasura-User-Id + - _or: + - team_id: + _is_null: true + - team: + members: + user_id: + _eq: X-Hasura-User-Id delete_permissions: - role: user permission: @@ -778,16 +806,22 @@ - name filter: _or: - - user_id: - _eq: X-Hasura-User-Id + - _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id - team: members: user_id: _eq: X-Hasura-User-Id check: _or: - - user_id: - _eq: X-Hasura-User-Id + - _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id - team: members: user_id: @@ -797,8 +831,11 @@ permission: filter: _or: - - user_id: - _eq: X-Hasura-User-Id + - _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id - team: members: user_id: @@ -898,8 +935,11 @@ filter: _or: - datasource: - user_id: - _eq: X-Hasura-User-Id + _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id - datasource: team: members: @@ -1149,13 +1189,27 @@ - role: user permission: check: - _or: - - user_id: - _eq: X-Hasura-User-Id - - team: - members: - user_id: - _eq: X-Hasura-User-Id + _and: + - exploration: + _or: + - datasource: + _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id + - datasource: + team: + members: + user_id: + _eq: X-Hasura-User-Id + - _or: + - team_id: + _is_null: true + - team: + members: + user_id: + _eq: X-Hasura-User-Id set: user_id: x-hasura-user-id columns: @@ -1208,13 +1262,27 @@ user_id: _eq: X-Hasura-User-Id check: - _or: - - user_id: - _eq: X-Hasura-User-Id - - team: - members: - user_id: - _eq: X-Hasura-User-Id + _and: + - exploration: + _or: + - datasource: + _and: + - team_id: + _is_null: true + - user_id: + _eq: X-Hasura-User-Id + - datasource: + team: + members: + user_id: + _eq: X-Hasura-User-Id + - _or: + - team_id: + _is_null: true + - team: + members: + user_id: + _eq: X-Hasura-User-Id delete_permissions: - role: user permission: diff --git a/tests/stepci/owner_flow.yml b/tests/stepci/owner_flow.yml index 5ee6731b..0ce6aba0 100644 --- a/tests/stepci/owner_flow.yml +++ b/tests/stepci/owner_flow.yml @@ -124,7 +124,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.accessToken}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userId}} graphql: query: | diff --git a/tests/workflows/default-models/customization.yml b/tests/workflows/default-models/customization.yml index e6da0988..8ab2edf7 100644 --- a/tests/workflows/default-models/customization.yml +++ b/tests/workflows/default-models/customization.yml @@ -189,7 +189,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.accessToken}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userId}} graphql: query: | diff --git a/tests/workflows/default-models/dynamic-access.yml b/tests/workflows/default-models/dynamic-access.yml index 6d909d46..73d68c59 100644 --- a/tests/workflows/default-models/dynamic-access.yml +++ b/tests/workflows/default-models/dynamic-access.yml @@ -227,7 +227,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.accessToken}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userId}} graphql: query: | @@ -449,7 +449,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.tokenB}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userB}} graphql: query: | diff --git a/tests/workflows/default-models/preprocessor.yml b/tests/workflows/default-models/preprocessor.yml index 7bfdd553..5404862d 100644 --- a/tests/workflows/default-models/preprocessor.yml +++ b/tests/workflows/default-models/preprocessor.yml @@ -189,7 +189,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.tokenA}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userA}} graphql: query: | @@ -294,7 +294,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.tokenB}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userB}} graphql: query: | diff --git a/tests/workflows/default-models/reconcile-noop.yml b/tests/workflows/default-models/reconcile-noop.yml index 62d5dc96..f130d2de 100644 --- a/tests/workflows/default-models/reconcile-noop.yml +++ b/tests/workflows/default-models/reconcile-noop.yml @@ -195,7 +195,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.accessToken}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userId}} graphql: query: | diff --git a/tests/workflows/default-models/reconcile-team.yml b/tests/workflows/default-models/reconcile-team.yml index 31b55ca3..851a7d8a 100644 --- a/tests/workflows/default-models/reconcile-team.yml +++ b/tests/workflows/default-models/reconcile-team.yml @@ -195,7 +195,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.accessToken}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userId}} graphql: query: | diff --git a/tests/workflows/default-models/rollout-halt.yml b/tests/workflows/default-models/rollout-halt.yml index b0446be7..d0a5166c 100644 --- a/tests/workflows/default-models/rollout-halt.yml +++ b/tests/workflows/default-models/rollout-halt.yml @@ -189,7 +189,7 @@ tests: method: POST headers: Content-Type: application/json - Authorization: Bearer ${{captures.accessToken}} + x-hasura-admin-secret: ${{env.HASURA_GRAPHQL_ADMIN_SECRET}} x-hasura-user-id: ${{captures.userId}} graphql: query: |