From 99355f8d74f2aae5690d9116bfe61f8eca8e29b5 Mon Sep 17 00:00:00 2001 From: Dave New Date: Tue, 6 Aug 2024 14:13:09 +0200 Subject: [PATCH] fix: headers, env vars and secrets in expressions (#1544) --- integration/testdata/env_vars/schema.keel | 10 +++ integration/testdata/env_vars/tests.test.ts | 23 ++++- integration/testdata/headers/schema.keel | 22 +++++ integration/testdata/headers/tests.test.ts | 90 ++++++++++++++++++++ integration/testdata/secrets/keelconfig.yaml | 2 - integration/testdata/secrets/schema.keel | 12 --- integration/testdata/secrets/tests.test.ts | 11 --- runtime/actions/authorization_test.go | 3 + runtime/expressions/operand.go | 13 +-- 9 files changed, 154 insertions(+), 32 deletions(-) create mode 100644 integration/testdata/headers/schema.keel create mode 100644 integration/testdata/headers/tests.test.ts delete mode 100644 integration/testdata/secrets/keelconfig.yaml delete mode 100644 integration/testdata/secrets/schema.keel delete mode 100644 integration/testdata/secrets/tests.test.ts diff --git a/integration/testdata/env_vars/schema.keel b/integration/testdata/env_vars/schema.keel index 30aa16c58..13255079c 100644 --- a/integration/testdata/env_vars/schema.keel +++ b/integration/testdata/env_vars/schema.keel @@ -8,5 +8,15 @@ model Person { @set(person.name = ctx.env.PERSON_NAME) @permission(expression: true) } + get getBob(id) { + @permission(expression: ctx.env.PERSON_NAME == "Bob") + } + get getPedro(id) { + @permission(expression: ctx.env.PERSON_NAME == "Pedro") + } + list listPedros() { + @where(person.name == ctx.env.PERSON_NAME) + @permission(expression: true) + } } } diff --git a/integration/testdata/env_vars/tests.test.ts b/integration/testdata/env_vars/tests.test.ts index d845b332a..3d30a29e2 100644 --- a/integration/testdata/env_vars/tests.test.ts +++ b/integration/testdata/env_vars/tests.test.ts @@ -1,10 +1,29 @@ -import { actions, resetDatabase } from "@teamkeel/testing"; +import { actions, resetDatabase, models } from "@teamkeel/testing"; import { beforeEach, expect, test } from "vitest"; beforeEach(resetDatabase); -test("create person with env var name", async () => { +test("set with env var", async () => { const person = await actions.createPerson({}); expect(person.name).toEqual("Pedro"); }); + +test("permissions with env var", async () => { + const person = await actions.createPerson({}); + + await expect( + actions.getPedro({ id: person.id }) + ).not.toHaveAuthorizationError(); + + await expect(actions.getBob({ id: person.id })).toHaveAuthorizationError(); +}); + +test("where with env var", async () => { + await models.person.create({ name: "Pedro" }); + await models.person.create({ name: "Bob" }); + + const pedros = await actions.listPedros(); + expect(pedros.results.length).toEqual(1); + expect(pedros.results[0].name).toEqual("Pedro"); +}); diff --git a/integration/testdata/headers/schema.keel b/integration/testdata/headers/schema.keel new file mode 100644 index 000000000..f5e644090 --- /dev/null +++ b/integration/testdata/headers/schema.keel @@ -0,0 +1,22 @@ +model Person { + fields { + name Text + } + + actions { + create createPerson() { + @set(person.name = ctx.headers.PERSON_NAME) + @permission(expression: true) + } + get getBob(id) { + @permission(expression: ctx.headers.PERSON_NAME == "Bob") + } + get getPedro(id) { + @permission(expression: ctx.headers.PERSON_NAME == "Pedro") + } + list listPedros() { + @where(person.name == ctx.headers.PERSON_NAME) + @permission(expression: true) + } + } +} diff --git a/integration/testdata/headers/tests.test.ts b/integration/testdata/headers/tests.test.ts new file mode 100644 index 000000000..2af8c4ec4 --- /dev/null +++ b/integration/testdata/headers/tests.test.ts @@ -0,0 +1,90 @@ +import { actions, resetDatabase, models } from "@teamkeel/testing"; +import { beforeEach, expect, test } from "vitest"; + +beforeEach(resetDatabase); + +test("set with http header", async () => { + const response = await fetch( + process.env.KEEL_TESTING_ACTIONS_API_URL + "/createPerson", + { + body: "{}", + method: "POST", + headers: { + "Content-Type": "application/json", + PERSON_NAME: "Pedro", + }, + } + ); + + expect(response.status).toEqual(200); + const data = await response.json(); + expect(data?.name).toEqual("Pedro"); +}); + +test("permissions with http header", async () => { + const response = await fetch( + process.env.KEEL_TESTING_ACTIONS_API_URL + "/createPerson", + { + body: "{}", + method: "POST", + headers: { + "Content-Type": "application/json", + PERSON_NAME: "Pedro", + }, + } + ); + + expect(response.status).toEqual(200); + const person = await response.json(); + + const getPedro = await fetch( + process.env.KEEL_TESTING_ACTIONS_API_URL + "/getPedro", + { + body: `{ "id": "${person.id}"}`, + method: "POST", + headers: { + "Content-Type": "application/json", + PERSON_NAME: "Pedro", + }, + } + ); + + expect(getPedro.status).toEqual(200); + + const getBob = await fetch( + process.env.KEEL_TESTING_ACTIONS_API_URL + "/getBob", + { + body: `{ "id": "${person.id}"}`, + method: "POST", + headers: { + "Content-Type": "application/json", + PERSON_NAME: "Pedro", + }, + } + ); + + expect(getBob.status).toEqual(403); +}); + +test("where with http header", async () => { + await models.person.create({ name: "Pedro" }); + await models.person.create({ name: "Bob" }); + + const listPedros = await fetch( + process.env.KEEL_TESTING_ACTIONS_API_URL + "/listPedros", + { + body: "{}", + method: "POST", + headers: { + "Content-Type": "application/json", + PERSON_NAME: "Pedro", + }, + } + ); + + expect(listPedros.status).toEqual(200); + + const pedros = await listPedros.json(); + expect(pedros.results.length).toEqual(1); + expect(pedros.results[0].name).toEqual("Pedro"); +}); diff --git a/integration/testdata/secrets/keelconfig.yaml b/integration/testdata/secrets/keelconfig.yaml deleted file mode 100644 index f22103f35..000000000 --- a/integration/testdata/secrets/keelconfig.yaml +++ /dev/null @@ -1,2 +0,0 @@ -secrets: - - name: TEST_API_KEY diff --git a/integration/testdata/secrets/schema.keel b/integration/testdata/secrets/schema.keel deleted file mode 100644 index 7fa792d7a..000000000 --- a/integration/testdata/secrets/schema.keel +++ /dev/null @@ -1,12 +0,0 @@ -model Person { - fields { - name Text - secretKey Text - } - - actions { - create createPerson() with (name, secretKey) { - @permission(expression: person.secretKey == ctx.secrets.TEST_API_KEY) - } - } -} diff --git a/integration/testdata/secrets/tests.test.ts b/integration/testdata/secrets/tests.test.ts deleted file mode 100644 index a0963ee9d..000000000 --- a/integration/testdata/secrets/tests.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { actions, resetDatabase } from "@teamkeel/testing"; -import { beforeEach, expect, test } from "vitest"; - -beforeEach(resetDatabase); - -test("create person with secret key", async () => { - const person = await actions.createPerson({ - name: "dave", - secretKey: "1232132_2323", - }); -}); diff --git a/runtime/actions/authorization_test.go b/runtime/actions/authorization_test.go index 95fe31266..183177c5b 100644 --- a/runtime/actions/authorization_test.go +++ b/runtime/actions/authorization_test.go @@ -9,6 +9,7 @@ import ( "github.com/teamkeel/keel/proto" "github.com/teamkeel/keel/runtime/actions" "github.com/teamkeel/keel/runtime/auth" + "github.com/teamkeel/keel/runtime/runtimectx" "github.com/teamkeel/keel/schema/parser" ) @@ -1427,6 +1428,8 @@ func TestPermissionQueryBuilder(t *testing.T) { ctx = auth.WithIdentity(ctx, testCase.identity) } + ctx = runtimectx.WithSecrets(ctx, map[string]string{"MY_SECRET": "1234"}) + scope, _, _, err := generateQueryScope(ctx, testCase.keelSchema, testCase.actionName) if err != nil { require.NoError(t, err) diff --git a/runtime/expressions/operand.go b/runtime/expressions/operand.go index 8d44f5716..84e8039d0 100644 --- a/runtime/expressions/operand.go +++ b/runtime/expressions/operand.go @@ -316,12 +316,15 @@ func (resolver *OperandResolver) GetOperandType() (proto.Type, bool, error) { return proto.Type_TYPE_UNKNOWN, false, fmt.Errorf("could not find explicit input %s on action %s", inputName, action.Name) } return field.Type.Type, field.Type.Repeated, nil + case resolver.operand.Ident.IsContextNowField(): + return proto.Type_TYPE_TIMESTAMP, false, nil + case resolver.operand.Ident.IsContextEnvField(): + return proto.Type_TYPE_STRING, false, nil + case resolver.operand.Ident.IsContextSecretField(): + return proto.Type_TYPE_STRING, false, nil + case resolver.operand.Ident.IsContextHeadersField(): + return proto.Type_TYPE_STRING, false, nil case operand.Ident.IsContext(): - fragmentCount := len(operand.Ident.Fragments) - if fragmentCount > 2 && operand.Ident.IsContextEnvField() { - return proto.Type_TYPE_STRING, false, nil - } - fieldName := operand.Ident.Fragments[1].Fragment return runtimectx.ContextFieldTypes[fieldName], false, nil default: