Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions services/actions/src/rpc/__tests__/teamAuthz.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
4 changes: 3 additions & 1 deletion services/actions/src/rpc/createTeam.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
};

Expand Down
17 changes: 13 additions & 4 deletions services/actions/src/rpc/exportDataModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || [];

Expand Down
18 changes: 14 additions & 4 deletions services/actions/src/rpc/sendTestAlert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions services/hasura/metadata/actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ actions:
definition:
kind: synchronous
handler: '{{ACTIONS_URL}}/rpc/export_data_models'
forward_client_headers: true
timeout: 60
permissions:
- role: user
Expand Down
140 changes: 104 additions & 36 deletions services/hasura/metadata/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/stepci/owner_flow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion tests/workflows/default-models/customization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
4 changes: 2 additions & 2 deletions tests/workflows/default-models/dynamic-access.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down
4 changes: 2 additions & 2 deletions tests/workflows/default-models/preprocessor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion tests/workflows/default-models/reconcile-noop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
Loading
Loading