From bec80de17486c418c409a7cfe3bd874e55be5049 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Apr 2023 16:51:50 -0700 Subject: [PATCH 1/6] add integ tests for fetching/secrets secrets with jwt/service token --- .github/workflows/docker-image.yml | 18 +- backend/package-lock.json | 14 +- backend/package.json | 6 +- backend/src/index.ts | 26 +-- backend/src/utils/addDevelopmentUser.ts | 21 +- backend/test-resources/env-vars.js | 5 + backend/tests/data/batch-secrets.json | 56 +++++ backend/tests/helper/helper.ts | 91 ++++++++ .../routes/v2/secrets.test.ts | 198 ++++++++++++++++++ .../{ => unit-tests}/utils/crypto.test.ts | 2 +- .../{ => unit-tests}/utils/posthog.test.ts | 2 +- 11 files changed, 398 insertions(+), 41 deletions(-) create mode 100644 backend/tests/data/batch-secrets.json create mode 100644 backend/tests/helper/helper.ts create mode 100644 backend/tests/integration-tests/routes/v2/secrets.test.ts rename backend/tests/{ => unit-tests}/utils/crypto.test.ts (99%) rename backend/tests/{ => unit-tests}/utils/posthog.test.ts (93%) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b4f9546ae..0c06bf342 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,6 +9,12 @@ jobs: steps: - name: ☁️ Checkout source uses: actions/checkout@v3 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: 🧪 Run tests + run: npm run test:ci + working-directory: backend - name: Save commit hashes for tag id: commit uses: pr-mpt/actions-commit-hash@v2 @@ -45,8 +51,8 @@ jobs: token: ${{ secrets.DEPOT_PROJECT_TOKEN }} push: true context: backend - tags: infisical/backend:${{ steps.commit.outputs.short }}, - infisical/backend:latest + tags: infisical/backend:${{ steps.commit.outputs.short }}, + infisical/backend:latest platforms: linux/amd64,linux/arm64 frontend-image: @@ -94,8 +100,8 @@ jobs: push: true token: ${{ secrets.DEPOT_PROJECT_TOKEN }} context: frontend - tags: infisical/frontend:${{ steps.commit.outputs.short }}, - infisical/frontend:latest + tags: infisical/frontend:${{ steps.commit.outputs.short }}, + infisical/frontend:latest platforms: linux/amd64,linux/arm64 build-args: | POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} @@ -122,7 +128,7 @@ jobs: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Save DigitalOcean kubeconfig with short-lived credentials run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179 - - name: switch to gamma namespace + - name: switch to gamma namespace run: kubectl config set-context --current --namespace=gamma - name: test kubectl run: kubectl get ingress @@ -135,4 +141,4 @@ jobs: exit 1 else echo "Helm upgrade was successful" - fi \ No newline at end of file + fi diff --git a/backend/package-lock.json b/backend/package-lock.json index 93146e5fc..ab41b74b0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -62,7 +62,7 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", - "@types/jest": "^29.2.4", + "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", @@ -3629,9 +3629,9 @@ } }, "node_modules/@types/jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz", - "integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -15642,9 +15642,9 @@ } }, "@types/jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz", - "integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", "dev": true, "requires": { "expect": "^29.0.0", diff --git a/backend/package.json b/backend/package.json index c740364d7..ef6e348b7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,8 +3,8 @@ "@aws-sdk/client-secrets-manager": "^3.287.0", "@godaddy/terminus": "^4.11.2", "@octokit/rest": "^19.0.5", - "@sentry/tracing": "^7.39.0", "@sentry/node": "^7.40.0", + "@sentry/tracing": "^7.39.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "await-to-js": "^3.0.0", @@ -57,7 +57,7 @@ "lint-and-fix": "eslint . --ext .ts --fix", "lint-staged": "lint-staged", "pretest": "docker compose -f test-resources/docker-compose.test.yml up -d", - "test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles", + "test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest", "test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json", "posttest": "docker compose -f test-resources/docker-compose.test.yml down" }, @@ -80,7 +80,7 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", - "@types/jest": "^29.2.4", + "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", diff --git a/backend/src/index.ts b/backend/src/index.ts index 56fcc288a..912d39177 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -48,18 +48,18 @@ import { integrationAuth as v1IntegrationAuthRouter } from './routes/v1'; import { - signup as v2SignupRouter, - auth as v2AuthRouter, - users as v2UsersRouter, - organizations as v2OrganizationsRouter, - workspace as v2WorkspaceRouter, - secret as v2SecretRouter, // begin to phase out - secrets as v2SecretsRouter, - serviceTokenData as v2ServiceTokenDataRouter, - serviceAccounts as v2ServiceAccountsRouter, - apiKeyData as v2APIKeyDataRouter, - environment as v2EnvironmentRouter, - tags as v2TagsRouter, + signup as v2SignupRouter, + auth as v2AuthRouter, + users as v2UsersRouter, + organizations as v2OrganizationsRouter, + workspace as v2WorkspaceRouter, + secret as v2SecretRouter, // begin to phase out + secrets as v2SecretsRouter, + serviceTokenData as v2ServiceTokenDataRouter, + serviceAccounts as v2ServiceAccountsRouter, + apiKeyData as v2APIKeyDataRouter, + environment as v2EnvironmentRouter, + tags as v2TagsRouter, } from './routes/v2'; import { healthCheck } from './routes/status'; import { getLogger } from './utils/logger'; @@ -172,7 +172,7 @@ const main = async () => { getLogger("backend-main").info(`Server started listening at port ${getPort()}`) }); - createTestUserForDevelopment(); + await createTestUserForDevelopment(); setUpHealthEndpoint(server); server.on('close', async () => { diff --git a/backend/src/utils/addDevelopmentUser.ts b/backend/src/utils/addDevelopmentUser.ts index 585740a6b..136d91a98 100644 --- a/backend/src/utils/addDevelopmentUser.ts +++ b/backend/src/utils/addDevelopmentUser.ts @@ -8,17 +8,18 @@ import { Key, Membership, MembershipOrg, Organization, User, Workspace } from ". import { Types } from 'mongoose'; import { getNodeEnv } from '../config'; -export const createTestUserForDevelopment = async () => { - if (getNodeEnv() === "development") { - const testUserEmail = "test@localhost.local" - const testUserPassword = "testInfisical1" - const testUserId = "63cefa6ec8d3175601cfa980" - const testWorkspaceId = "63cefb15c8d3175601cfa989" - const testOrgId = "63cefb15c8d3175601cfa985" - const testMembershipId = "63cefb159185d9aa3ef0cf35" - const testMembershipOrgId = "63cefb159185d9aa3ef0cf31" - const testWorkspaceKeyId = "63cf48f0225e6955acec5eff" +export const testUserEmail = "test@localhost.local" +export const testUserPassword = "testInfisical1" +export const testUserId = "63cefa6ec8d3175601cfa980" +export const testWorkspaceId = "63cefb15c8d3175601cfa989" +export const testOrgId = "63cefb15c8d3175601cfa985" +export const testMembershipId = "63cefb159185d9aa3ef0cf35" +export const testMembershipOrgId = "63cefb159185d9aa3ef0cf31" +export const testWorkspaceKeyId = "63cf48f0225e6955acec5eff" +export const plainTextWorkspaceKey = "543fef8224813a46230b0a50a46c5fb2" +export const createTestUserForDevelopment = async () => { + if (getNodeEnv() === "development" || getNodeEnv() === "test") { const testUser = { _id: testUserId, email: testUserEmail, diff --git a/backend/test-resources/env-vars.js b/backend/test-resources/env-vars.js index a7542728f..6702649c1 100644 --- a/backend/test-resources/env-vars.js +++ b/backend/test-resources/env-vars.js @@ -3,3 +3,8 @@ process.env.MONGO_URL = 'mongodb://test:test1234@localhost:27018/?authSource=admin'; process.env.MONGO_USERNAME = 'test'; process.env.MONGO_PASSWORD = 'test1234'; +process.env.NODE_ENV = 'test'; +process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392" +process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364" +process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2" +process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5" \ No newline at end of file diff --git a/backend/tests/data/batch-secrets.json b/backend/tests/data/batch-secrets.json new file mode 100644 index 000000000..7236c907a --- /dev/null +++ b/backend/tests/data/batch-secrets.json @@ -0,0 +1,56 @@ +[ + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + } +] \ No newline at end of file diff --git a/backend/tests/helper/helper.ts b/backend/tests/helper/helper.ts new file mode 100644 index 000000000..483f1a5ce --- /dev/null +++ b/backend/tests/helper/helper.ts @@ -0,0 +1,91 @@ +// Helper functions for integration tests + +import { Secret } from "../../src/models"; +import { testUserEmail, testUserPassword } from "../../src/utils/addDevelopmentUser"; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const crypto = require('crypto') +// eslint-disable-next-line @typescript-eslint/no-var-requires +const jsrp = require('jsrp'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const axios = require('axios'); +import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser"; +import { encryptSymmetric } from "../../src/utils/crypto"; + +interface TokenData { + token: string; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + tag: string; +} + +export const getJWTFromTestUser = (): Promise => { + return new Promise((resolve, reject) => { + const client = new jsrp.client(); + const EMAIL = testUserEmail + const PASSWORD = testUserPassword + + client.init({ + username: EMAIL, + password: PASSWORD, + }, async () => { + const clientPublicKey = client.getPublicKey(); + + // POST: /login1 + const reqBody = { + email: EMAIL, + clientPublicKey + } + + + const loginOneRes = await axios.post('http://localhost:4000/api/v1/auth/login1', reqBody); + const serverPublicKey = loginOneRes.data.serverPublicKey; + const salt = loginOneRes.data.salt; + + client.setSalt(salt); + client.setServerPublicKey(serverPublicKey); + const clientSharedKey = client.getSharedKey(); // shared Key + const clientProof = client.getProof(); // called M1 + + // POST: /login2 + const reqBody2 = { + email: EMAIL, + clientProof + } + + const response2 = await axios.post('http://localhost:4000/api/v1/auth/login2', reqBody2); + + resolve(response2.data) + }) + }); +} + +export const getServiceTokenFromTestUser = async () => { + const loggedInUserDetails = await getJWTFromTestUser() + const randomBytes = crypto.randomBytes(16).toString('hex'); + const { ciphertext, iv, tag } = encryptSymmetric({ + plaintext: plainTextWorkspaceKey, + key: randomBytes, + }); + + const newServiceToken = await axios.post('http://localhost:4000/api/v2/service-token/', { + 'name': "test service token", + 'workspaceId': testWorkspaceId, + 'environment': "dev", + 'encryptedKey': ciphertext, + 'iv': iv, + 'tag': tag, + 'expiresIn': Date.now() + 90000, + 'permissions': ["read"] + }, { + headers: { + 'Authorization': `Bearer ${loggedInUserDetails.token}` + } + }); + + return `${newServiceToken.data.serviceToken}.${randomBytes}` +} + +export const deleteAllSecrets = async () => { + await Secret.deleteMany() +} \ No newline at end of file diff --git a/backend/tests/integration-tests/routes/v2/secrets.test.ts b/backend/tests/integration-tests/routes/v2/secrets.test.ts new file mode 100644 index 000000000..b380e9f9c --- /dev/null +++ b/backend/tests/integration-tests/routes/v2/secrets.test.ts @@ -0,0 +1,198 @@ +import request from 'supertest' +import main from '../../../../src/index' +import { testWorkspaceId } from '../../../../src/utils/addDevelopmentUser'; +import { deleteAllSecrets, getJWTFromTestUser, getServiceTokenFromTestUser } from '../../../helper/helper'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const batchSecretRequest = require('../../../data/batch-secrets.json'); + +let server: any; + +beforeAll(async () => { + server = await main; +}); + +afterAll(async () => { + server.close(); +}); + +describe("GET /api/v2/secrets", () => { + describe("Get secrets via JTW with no personal secrets", () => { + test("should respond with a 200 status code", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequest + }) + + expect(createSecretsResponse.statusCode).toBe(200) + + + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${loginResponse.token}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) + + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(3) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); + + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); + + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); + + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); + + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); + + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); + + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); + + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); + + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); + + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } + }) + }) + + describe("fetch secrets via service token with no personal secrets", () => { + test("should respond with a 200 status code", async () => { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequest + }) + + expect(createSecretsResponse.statusCode).toBe(200) + + + // now use the service token to fetch secrets + const serviceToken = await getServiceTokenFromTestUser() + + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${serviceToken}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) + + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(3) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); + + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); + + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); + + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); + + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); + + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); + + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); + + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); + + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); + + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + }) + }) +}) \ No newline at end of file diff --git a/backend/tests/utils/crypto.test.ts b/backend/tests/unit-tests/utils/crypto.test.ts similarity index 99% rename from backend/tests/utils/crypto.test.ts rename to backend/tests/unit-tests/utils/crypto.test.ts index aea5f5f26..bbbfd0297 100644 --- a/backend/tests/utils/crypto.test.ts +++ b/backend/tests/unit-tests/utils/crypto.test.ts @@ -4,7 +4,7 @@ import { decryptSymmetric, encryptAsymmetric, encryptSymmetric -} from '../../src/utils/crypto'; +} from '../../../src/utils/crypto'; describe('Crypto', () => { describe('encryptAsymmetric', () => { diff --git a/backend/tests/utils/posthog.test.ts b/backend/tests/unit-tests/utils/posthog.test.ts similarity index 93% rename from backend/tests/utils/posthog.test.ts rename to backend/tests/unit-tests/utils/posthog.test.ts index a380060b0..9f202385c 100644 --- a/backend/tests/utils/posthog.test.ts +++ b/backend/tests/unit-tests/utils/posthog.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from '@jest/globals'; -import { getChannelFromUserAgent } from '../../src/utils/posthog'; +import { getChannelFromUserAgent } from '../../../src/utils/posthog'; describe('posthog getChannelFromUserAgent', () => { test("should return 'web' when userAgent includes 'mozilla'", () => { From 8c450d51da4041aec32f445d47ed03dfc0b0d613 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Apr 2023 17:49:45 -0700 Subject: [PATCH 2/6] add integration tests for service-tokens --- .../routes/v2/secrets.test.ts | 6 +- .../routes/v2/service-tokens.test.ts | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 backend/tests/integration-tests/routes/v2/service-tokens.test.ts diff --git a/backend/tests/integration-tests/routes/v2/secrets.test.ts b/backend/tests/integration-tests/routes/v2/secrets.test.ts index b380e9f9c..bdfcf24f6 100644 --- a/backend/tests/integration-tests/routes/v2/secrets.test.ts +++ b/backend/tests/integration-tests/routes/v2/secrets.test.ts @@ -17,7 +17,7 @@ afterAll(async () => { describe("GET /api/v2/secrets", () => { describe("Get secrets via JTW with no personal secrets", () => { - test("should respond with a 200 status code", async () => { + test("should create secrets and read secrets via jwt", async () => { try { // get login details const loginResponse = await getJWTFromTestUser() @@ -108,11 +108,11 @@ describe("GET /api/v2/secrets", () => { }) describe("fetch secrets via service token with no personal secrets", () => { - test("should respond with a 200 status code", async () => { + test("should create secrets and read secrets via service token", async () => { // get login details const loginResponse = await getJWTFromTestUser() - // create creates + // create secrets const createSecretsResponse = await request(server) .post("/api/v2/secrets/batch") .set('Authorization', `Bearer ${loginResponse.token}`) diff --git a/backend/tests/integration-tests/routes/v2/service-tokens.test.ts b/backend/tests/integration-tests/routes/v2/service-tokens.test.ts new file mode 100644 index 000000000..b011db36f --- /dev/null +++ b/backend/tests/integration-tests/routes/v2/service-tokens.test.ts @@ -0,0 +1,58 @@ +import request from 'supertest' +import main from '../../../../src/index' +import { getServiceTokenFromTestUser } from '../../../helper/helper'; +let server: any; + +beforeAll(async () => { + server = await main; +}); + +afterAll(async () => { + server.close(); +}); + +describe("GET /api/v2/service-token", () => { + describe("Get service token details", () => { + test("should respond create and get the details of a service token", async () => { + // generate a service token + const serviceToken = await getServiceTokenFromTestUser() + + // get the service token details + const serviceTokenDetails = await request(server) + .get("/api/v2/service-token") + .set('Authorization', `Bearer ${serviceToken}`) + + expect(serviceTokenDetails.body).toMatchObject({ + _id: expect.any(String), + name: 'test service token', + workspace: '63cefb15c8d3175601cfa989', + environment: 'dev', + user: { + _id: '63cefa6ec8d3175601cfa980', + email: 'test@localhost.local', + firstName: 'Jake', + lastName: 'Moni', + isMfaEnabled: false, + mfaMethods: expect.any(Array), + devices: [ + { + ip: expect.any(String), + userAgent: expect.any(String), + _id: expect.any(String), + }, + ], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }, + lastUsed: expect.any(String), + expiresAt: expect.any(String), + encryptedKey: expect.any(String), + iv: expect.any(String), + tag: expect.any(String), + permissions: ['read'], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }) + }) +}) \ No newline at end of file From 73ddad8dac69dbe647c5c837620a489a53374177 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Apr 2023 18:12:20 -0700 Subject: [PATCH 3/6] update service token tests --- .../routes/v2/{service-tokens.test.ts => service-tokens.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/tests/integration-tests/routes/v2/{service-tokens.test.ts => service-tokens.ts} (100%) diff --git a/backend/tests/integration-tests/routes/v2/service-tokens.test.ts b/backend/tests/integration-tests/routes/v2/service-tokens.ts similarity index 100% rename from backend/tests/integration-tests/routes/v2/service-tokens.test.ts rename to backend/tests/integration-tests/routes/v2/service-tokens.ts From c7a402c4cb484fe06e46778287cbfcf6cae5e571 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 11 Apr 2023 18:50:01 -0700 Subject: [PATCH 4/6] add integ tests for service token with overrides --- ...ts.json => batch-secrets-no-override.json} | 0 .../data/batch-secrets-with-overrides.json | 38 +++ backend/tests/helper/helper.ts | 7 +- .../routes/v2/secrets.test.ts | 321 ++++++++++++++---- 4 files changed, 294 insertions(+), 72 deletions(-) rename backend/tests/data/{batch-secrets.json => batch-secrets-no-override.json} (100%) create mode 100644 backend/tests/data/batch-secrets-with-overrides.json diff --git a/backend/tests/data/batch-secrets.json b/backend/tests/data/batch-secrets-no-override.json similarity index 100% rename from backend/tests/data/batch-secrets.json rename to backend/tests/data/batch-secrets-no-override.json diff --git a/backend/tests/data/batch-secrets-with-overrides.json b/backend/tests/data/batch-secrets-with-overrides.json new file mode 100644 index 000000000..6173289aa --- /dev/null +++ b/backend/tests/data/batch-secrets-with-overrides.json @@ -0,0 +1,38 @@ +[ + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "environment": "dev", + "secretKeyCiphertext": "IVMtGWE=", + "secretKeyIV": "BDsG7/ylk7mT8MrIMn0e7w==", + "secretKeyTag": "1ujy08fctmZ1xTXMYr23UQ==", + "secretValueCiphertext": "I9psUg==", + "secretValueIV": "W+DJETpCerHkFv8AR9Fv4w==", + "secretValueTag": "yODOeN3HBr/usly4VSMt9w==", + "secretCommentCiphertext": "", + "secretCommentIV": "QET7oX2ZiuLDSzwrkeL2Ig==", + "secretCommentTag": "6P3xeA9eO+3Wp66ROHXgfg==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "personal", + "user": "63cefa6ec8d3175601cfa980", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "Q7lyRO8=", + "secretKeyIV": "yz8koc3d63ywJMiGXpCNSw==", + "secretKeyTag": "j2bMQ2d4sDZKA0OaKM5SXA==", + "secretValueCiphertext": "X4kaiShmtGZt", + "secretValueIV": "p/GdbksLVveNLsV3vz5GLA==", + "secretValueTag": "//dhRL+pagecavHJCtMPWg==", + "secretCommentCiphertext": "", + "secretCommentIV": "7eYJzuilvjQPutqrqbd2MQ==", + "secretCommentTag": "LpPv9K0Hhd5noE39Zu9U+w==" + } + } +] \ No newline at end of file diff --git a/backend/tests/helper/helper.ts b/backend/tests/helper/helper.ts index 483f1a5ce..4f3371d55 100644 --- a/backend/tests/helper/helper.ts +++ b/backend/tests/helper/helper.ts @@ -1,5 +1,6 @@ // Helper functions for integration tests +import axiosInstance from "../../src/config/request"; import { Secret } from "../../src/models"; import { testUserEmail, testUserPassword } from "../../src/utils/addDevelopmentUser"; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -38,7 +39,7 @@ export const getJWTFromTestUser = (): Promise => { } - const loginOneRes = await axios.post('http://localhost:4000/api/v1/auth/login1', reqBody); + const loginOneRes = await axiosInstance.post('http://localhost:4000/api/v1/auth/login1', reqBody); const serverPublicKey = loginOneRes.data.serverPublicKey; const salt = loginOneRes.data.salt; @@ -53,7 +54,7 @@ export const getJWTFromTestUser = (): Promise => { clientProof } - const response2 = await axios.post('http://localhost:4000/api/v1/auth/login2', reqBody2); + const response2 = await axiosInstance.post('http://localhost:4000/api/v1/auth/login2', reqBody2); resolve(response2.data) }) @@ -68,7 +69,7 @@ export const getServiceTokenFromTestUser = async () => { key: randomBytes, }); - const newServiceToken = await axios.post('http://localhost:4000/api/v2/service-token/', { + const newServiceToken = await axiosInstance.post('http://localhost:4000/api/v2/service-token/', { 'name': "test service token", 'workspaceId': testWorkspaceId, 'environment': "dev", diff --git a/backend/tests/integration-tests/routes/v2/secrets.test.ts b/backend/tests/integration-tests/routes/v2/secrets.test.ts index bdfcf24f6..0ceda45de 100644 --- a/backend/tests/integration-tests/routes/v2/secrets.test.ts +++ b/backend/tests/integration-tests/routes/v2/secrets.test.ts @@ -3,7 +3,9 @@ import main from '../../../../src/index' import { testWorkspaceId } from '../../../../src/utils/addDevelopmentUser'; import { deleteAllSecrets, getJWTFromTestUser, getServiceTokenFromTestUser } from '../../../helper/helper'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const batchSecretRequest = require('../../../data/batch-secrets.json'); +const batchSecretRequestWithNoOverride = require('../../../data/batch-secrets-no-override.json'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const batchSecretRequestWithOverrides = require('../../../data/batch-secrets-with-overrides.json'); let server: any; @@ -16,7 +18,7 @@ afterAll(async () => { }); describe("GET /api/v2/secrets", () => { - describe("Get secrets via JTW with no personal secrets", () => { + describe("Get secrets via JTW", () => { test("should create secrets and read secrets via jwt", async () => { try { // get login details @@ -29,7 +31,7 @@ describe("GET /api/v2/secrets", () => { .send({ workspaceId: testWorkspaceId, environment: "dev", - requests: batchSecretRequest + requests: batchSecretRequestWithNoOverride }) expect(createSecretsResponse.statusCode).toBe(200) @@ -105,94 +107,275 @@ describe("GET /api/v2/secrets", () => { await deleteAllSecrets() } }) - }) - describe("fetch secrets via service token with no personal secrets", () => { - test("should create secrets and read secrets via service token", async () => { - // get login details - const loginResponse = await getJWTFromTestUser() + test("Get secrets via jwt when personal overrides exist", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() - // create secrets - const createSecretsResponse = await request(server) - .post("/api/v2/secrets/batch") - .set('Authorization', `Bearer ${loginResponse.token}`) - .send({ - workspaceId: testWorkspaceId, - environment: "dev", - requests: batchSecretRequest - }) + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithOverrides + }) - expect(createSecretsResponse.statusCode).toBe(200) + expect(createSecretsResponse.statusCode).toBe(200) + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${loginResponse.token}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) - // now use the service token to fetch secrets - const serviceToken = await getServiceTokenFromTestUser() + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(2) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); - const getSecrets = await request(server) - .get("/api/v2/secrets") - .set('Authorization', `Bearer ${serviceToken}`) - .query({ - workspaceId: testWorkspaceId, - environment: "dev" - }) + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); - expect(getSecrets.statusCode).toBe(200) - expect(getSecrets.body).toHaveProperty("secrets") - expect(getSecrets.body.secrets).toHaveLength(3) - expect(getSecrets.body.secrets).toBeInstanceOf(Array); + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); - getSecrets.body.secrets.forEach((secret: any) => { - expect(secret).toHaveProperty('_id'); - expect(secret._id).toBeTruthy(); + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); - expect(secret).toHaveProperty('version'); - expect(secret.version).toBeTruthy(); + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); - expect(secret).toHaveProperty('workspace'); - expect(secret.workspace).toBeTruthy(); + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); - expect(secret).toHaveProperty('type'); - expect(secret.type).toBeTruthy(); + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); - expect(secret).toHaveProperty('tags'); - expect(secret.tags).toHaveLength(0); + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); - expect(secret).toHaveProperty('environment'); - expect(secret.environment).toEqual("dev"); + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); - expect(secret).toHaveProperty('secretKeyCiphertext'); - expect(secret.secretKeyCiphertext).toBeTruthy(); + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); - expect(secret).toHaveProperty('secretKeyIV'); - expect(secret.secretKeyIV).toBeTruthy(); + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); - expect(secret).toHaveProperty('secretKeyTag'); - expect(secret.secretKeyTag).toBeTruthy(); + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); - expect(secret).toHaveProperty('secretValueCiphertext'); - expect(secret.secretValueCiphertext).toBeTruthy(); + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); - expect(secret).toHaveProperty('secretValueIV'); - expect(secret.secretValueIV).toBeTruthy(); + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); - expect(secret).toHaveProperty('secretValueTag'); - expect(secret.secretValueTag).toBeTruthy(); + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); - expect(secret).toHaveProperty('secretCommentCiphertext'); - expect(secret.secretCommentCiphertext).toBeFalsy(); + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); - expect(secret).toHaveProperty('secretCommentIV'); - expect(secret.secretCommentIV).toBeTruthy(); + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); - expect(secret).toHaveProperty('secretCommentTag'); - expect(secret.secretCommentTag).toBeTruthy(); - - expect(secret).toHaveProperty('createdAt'); - expect(secret.createdAt).toBeTruthy(); - - expect(secret).toHaveProperty('updatedAt'); - expect(secret.updatedAt).toBeTruthy(); - }); + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } }) }) + + describe("fetch secrets via service token", () => { + test("Get secrets via jwt when personal overrides exist", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithOverrides + }) + + expect(createSecretsResponse.statusCode).toBe(200) + + // now use the service token to fetch secrets + const serviceToken = await getServiceTokenFromTestUser() + + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${serviceToken}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) + + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(2) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); + + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); + + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); + + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); + + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); + + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); + + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); + + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); + + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); + + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } + }) + + // test("should create secrets and read secrets via service token when no overrides", async () => { + // // get login details + // const loginResponse = await getJWTFromTestUser() + + // // create secrets + // const createSecretsResponse = await request(server) + // .post("/api/v2/secrets/batch") + // .set('Authorization', `Bearer ${loginResponse.token}`) + // .send({ + // workspaceId: testWorkspaceId, + // environment: "dev", + // requests: batchSecretRequestWithNoOverride + // }) + + // expect(createSecretsResponse.statusCode).toBe(200) + + + // // now use the service token to fetch secrets + // const serviceToken = await getServiceTokenFromTestUser() + + // const getSecrets = await request(server) + // .get("/api/v2/secrets") + // .set('Authorization', `Bearer ${serviceToken}`) + // .query({ + // workspaceId: testWorkspaceId, + // environment: "dev" + // }) + + // expect(getSecrets.statusCode).toBe(200) + // expect(getSecrets.body).toHaveProperty("secrets") + // expect(getSecrets.body.secrets).toHaveLength(3) + // expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + // getSecrets.body.secrets.forEach((secret: any) => { + // expect(secret).toHaveProperty('_id'); + // expect(secret._id).toBeTruthy(); + + // expect(secret).toHaveProperty('version'); + // expect(secret.version).toBeTruthy(); + + // expect(secret).toHaveProperty('workspace'); + // expect(secret.workspace).toBeTruthy(); + + // expect(secret).toHaveProperty('type'); + // expect(secret.type).toBeTruthy(); + + // expect(secret).toHaveProperty('tags'); + // expect(secret.tags).toHaveLength(0); + + // expect(secret).toHaveProperty('environment'); + // expect(secret.environment).toEqual("dev"); + + // expect(secret).toHaveProperty('secretKeyCiphertext'); + // expect(secret.secretKeyCiphertext).toBeTruthy(); + + // expect(secret).toHaveProperty('secretKeyIV'); + // expect(secret.secretKeyIV).toBeTruthy(); + + // expect(secret).toHaveProperty('secretKeyTag'); + // expect(secret.secretKeyTag).toBeTruthy(); + + // expect(secret).toHaveProperty('secretValueCiphertext'); + // expect(secret.secretValueCiphertext).toBeTruthy(); + + // expect(secret).toHaveProperty('secretValueIV'); + // expect(secret.secretValueIV).toBeTruthy(); + + // expect(secret).toHaveProperty('secretValueTag'); + // expect(secret.secretValueTag).toBeTruthy(); + + // expect(secret).toHaveProperty('secretCommentCiphertext'); + // expect(secret.secretCommentCiphertext).toBeFalsy(); + + // expect(secret).toHaveProperty('secretCommentIV'); + // expect(secret.secretCommentIV).toBeTruthy(); + + // expect(secret).toHaveProperty('secretCommentTag'); + // expect(secret.secretCommentTag).toBeTruthy(); + + // expect(secret).toHaveProperty('createdAt'); + // expect(secret.createdAt).toBeTruthy(); + + // expect(secret).toHaveProperty('updatedAt'); + // expect(secret.updatedAt).toBeTruthy(); + // }); + // }) + + + }) }) \ No newline at end of file From e2c67ffbef40ba78338a76f469b6b801d96f3327 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 12 Apr 2023 11:59:41 -0700 Subject: [PATCH 5/6] allow service tokens to continue to support overrides --- .../src/controllers/v2/secretsController.ts | 102 +++++++++--------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index b18073515..82b16edd6 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -550,7 +550,10 @@ export const getSecrets = async (req: Request, res: Response) => { const workspaceId = req.query.workspaceId as string; const environment = req.query.environment as string; - // tags logic + // secrets to return + let secrets: ISecret[] = []; + + // query tags table to get all tags ids for the tag names for the given workspace let tagIds = []; const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : []; if (tagNamesList != undefined && tagNamesList.length != 0) { @@ -561,71 +564,70 @@ export const getSecrets = async (req: Request, res: Response) => { }); } - let secrets: ISecret[] = []; - if (req.user) { // case: client authorization is via JWT - - let hasWriteOnlyAccess - if (!req.serviceTokenData) { - hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment) - const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment) - if (hasNoAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } + const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment) + const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment) + if (hasNoAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } - let secretQuery: any; - if (tagNamesList != undefined && tagNamesList.length != 0) { - const workspaceFromDB = await Tag.find({ workspace: workspaceId }) + const secretQuery: any = { + workspace: workspaceId, + environment, + $or: [ + { user: req.user._id }, // personal secrets for this user + { user: { $exists: false } } // shared secrets from workspace + ] + } - const tagIds = _.map(tagNamesList, (tagName) => { - const tag = _.find(workspaceFromDB, { slug: tagName }); - return tag ? tag.id : null; - }); - - secretQuery = { - workspace: workspaceId, - environment, - $or: [ - { user: req.user._id }, - { user: { $exists: false } } - ], - tags: { $in: tagIds }, - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } - } - } else { - secretQuery = { - workspace: workspaceId, - environment, - $or: [ - { user: req.user._id }, - { user: { $exists: false } } - ], - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } - } + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; } if (hasWriteOnlyAccess) { - // (i.e. you don't get values to decrypt since you can only write) + // only return the secret keys and not the values since user does not have right to see values secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags") } else { secrets = await Secret.find(secretQuery).populate("tags") } } - if (req.serviceAccount || req.serviceTokenData) { - // case: client authorization is either via service account or service token + // case: client authorization is via service token + if (req.serviceTokenData) { + const userId = req.serviceTokenData.user._id - secrets = await Secret.find({ - workspace: new Types.ObjectId(workspaceId), + const secretQuery: any = { + workspace: workspaceId, environment, - user: { - $exists: false - }, - ...(tagIds.length > 0 ? { tags: { $in: tagIds } } : {}), - type: SECRET_SHARED - }).populate("tags"); + $or: [ + { user: userId }, // personal secrets for this user + { user: { $exists: false } } // shared secrets from workspace + ] + } + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + // TODO check if service token has write only permission + + secrets = await Secret.find(secretQuery).populate("tags"); + } + + // case: client authorization is via service account + if (req.serviceAccount) { + const secretQuery: any = { + workspace: workspaceId, + environment, + user: { $exists: false } // shared secrets only from workspace + } + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + secrets = await Secret.find(secretQuery).populate("tags"); } const channel = getChannelFromUserAgent(req.headers['user-agent']) From dda5f75450946eab792be942cdfa867a76bca936 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 12 Apr 2023 12:00:27 -0700 Subject: [PATCH 6/6] add integration tests for checking service token with overrides --- backend/src/helpers/rateLimiter.ts | 18 +- ...eate-secrets-with-some-missing-params.json | 51 ++++++ backend/tests/helper/helper.ts | 4 + .../routes/v2/secrets.test.ts | 157 ++++++++++-------- 4 files changed, 160 insertions(+), 70 deletions(-) create mode 100644 backend/tests/data/batch-create-secrets-with-some-missing-params.json diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index 432bd5f97..073ba11bc 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -15,7 +15,7 @@ const apiLimiter = rateLimit({ }); // 10 requests per minute -const authLimiter = rateLimit({ +const authLimit = rateLimit({ windowMs: 60 * 1000, max: 10, standardHeaders: true, @@ -36,8 +36,16 @@ const passwordLimiter = rateLimit({ } }); -export { - apiLimiter, - authLimiter, - passwordLimiter +const authLimiter = (req: any, res: any, next: any) => { + if (process.env.NODE_ENV === 'production') { + authLimit(req, res, next); + } else { + next(); + } +}; + +export { + apiLimiter, + authLimiter, + passwordLimiter }; diff --git a/backend/tests/data/batch-create-secrets-with-some-missing-params.json b/backend/tests/data/batch-create-secrets-with-some-missing-params.json new file mode 100644 index 000000000..40d07827c --- /dev/null +++ b/backend/tests/data/batch-create-secrets-with-some-missing-params.json @@ -0,0 +1,51 @@ +[ + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + } +] \ No newline at end of file diff --git a/backend/tests/helper/helper.ts b/backend/tests/helper/helper.ts index 4f3371d55..c59c8d43b 100644 --- a/backend/tests/helper/helper.ts +++ b/backend/tests/helper/helper.ts @@ -89,4 +89,8 @@ export const getServiceTokenFromTestUser = async () => { export const deleteAllSecrets = async () => { await Secret.deleteMany() +} + +export const getAllSecrets = async () => { + return await Secret.find() } \ No newline at end of file diff --git a/backend/tests/integration-tests/routes/v2/secrets.test.ts b/backend/tests/integration-tests/routes/v2/secrets.test.ts index 0ceda45de..4df6056fe 100644 --- a/backend/tests/integration-tests/routes/v2/secrets.test.ts +++ b/backend/tests/integration-tests/routes/v2/secrets.test.ts @@ -1,14 +1,16 @@ import request from 'supertest' import main from '../../../../src/index' import { testWorkspaceId } from '../../../../src/utils/addDevelopmentUser'; -import { deleteAllSecrets, getJWTFromTestUser, getServiceTokenFromTestUser } from '../../../helper/helper'; +import { deleteAllSecrets, getAllSecrets, getJWTFromTestUser, getServiceTokenFromTestUser } from '../../../helper/helper'; // eslint-disable-next-line @typescript-eslint/no-var-requires const batchSecretRequestWithNoOverride = require('../../../data/batch-secrets-no-override.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires const batchSecretRequestWithOverrides = require('../../../data/batch-secrets-with-overrides.json'); -let server: any; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const batchSecretRequestWithBadRequest = require('../../../data/batch-create-secrets-with-some-missing-params.json'); +let server: any; beforeAll(async () => { server = await main; }); @@ -289,93 +291,118 @@ describe("GET /api/v2/secrets", () => { } }) - // test("should create secrets and read secrets via service token when no overrides", async () => { - // // get login details - // const loginResponse = await getJWTFromTestUser() + test("should create secrets and read secrets via service token when no overrides", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() - // // create secrets - // const createSecretsResponse = await request(server) - // .post("/api/v2/secrets/batch") - // .set('Authorization', `Bearer ${loginResponse.token}`) - // .send({ - // workspaceId: testWorkspaceId, - // environment: "dev", - // requests: batchSecretRequestWithNoOverride - // }) + // create secrets + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithNoOverride + }) - // expect(createSecretsResponse.statusCode).toBe(200) + expect(createSecretsResponse.statusCode).toBe(200) - // // now use the service token to fetch secrets - // const serviceToken = await getServiceTokenFromTestUser() + // now use the service token to fetch secrets + const serviceToken = await getServiceTokenFromTestUser() - // const getSecrets = await request(server) - // .get("/api/v2/secrets") - // .set('Authorization', `Bearer ${serviceToken}`) - // .query({ - // workspaceId: testWorkspaceId, - // environment: "dev" - // }) + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${serviceToken}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) - // expect(getSecrets.statusCode).toBe(200) - // expect(getSecrets.body).toHaveProperty("secrets") - // expect(getSecrets.body.secrets).toHaveLength(3) - // expect(getSecrets.body.secrets).toBeInstanceOf(Array); + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(3) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); - // getSecrets.body.secrets.forEach((secret: any) => { - // expect(secret).toHaveProperty('_id'); - // expect(secret._id).toBeTruthy(); + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); - // expect(secret).toHaveProperty('version'); - // expect(secret.version).toBeTruthy(); + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); - // expect(secret).toHaveProperty('workspace'); - // expect(secret.workspace).toBeTruthy(); + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); - // expect(secret).toHaveProperty('type'); - // expect(secret.type).toBeTruthy(); + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); - // expect(secret).toHaveProperty('tags'); - // expect(secret.tags).toHaveLength(0); + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); - // expect(secret).toHaveProperty('environment'); - // expect(secret.environment).toEqual("dev"); + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); - // expect(secret).toHaveProperty('secretKeyCiphertext'); - // expect(secret.secretKeyCiphertext).toBeTruthy(); + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); - // expect(secret).toHaveProperty('secretKeyIV'); - // expect(secret.secretKeyIV).toBeTruthy(); + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); - // expect(secret).toHaveProperty('secretKeyTag'); - // expect(secret.secretKeyTag).toBeTruthy(); + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); - // expect(secret).toHaveProperty('secretValueCiphertext'); - // expect(secret.secretValueCiphertext).toBeTruthy(); + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); - // expect(secret).toHaveProperty('secretValueIV'); - // expect(secret.secretValueIV).toBeTruthy(); + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); - // expect(secret).toHaveProperty('secretValueTag'); - // expect(secret.secretValueTag).toBeTruthy(); + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); - // expect(secret).toHaveProperty('secretCommentCiphertext'); - // expect(secret.secretCommentCiphertext).toBeFalsy(); + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); - // expect(secret).toHaveProperty('secretCommentIV'); - // expect(secret.secretCommentIV).toBeTruthy(); + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); - // expect(secret).toHaveProperty('secretCommentTag'); - // expect(secret.secretCommentTag).toBeTruthy(); + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); - // expect(secret).toHaveProperty('createdAt'); - // expect(secret.createdAt).toBeTruthy(); + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); - // expect(secret).toHaveProperty('updatedAt'); - // expect(secret.updatedAt).toBeTruthy(); - // }); - // }) + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } + }) + }) + describe("create secrets via JWT", () => { + test("Create secrets via jwt when some requests have missing required parameters", async () => { + // get login details + const loginResponse = await getJWTFromTestUser() + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithBadRequest + }) + + const allSecretsInDB = await getAllSecrets() + + expect(createSecretsResponse.statusCode).toBe(500) // TODO should be set to 400 + expect(allSecretsInDB).toHaveLength(0) + }) }) }) \ No newline at end of file