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/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']) 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/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-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/data/batch-secrets-no-override.json b/backend/tests/data/batch-secrets-no-override.json new file mode 100644 index 000000000..7236c907a --- /dev/null +++ b/backend/tests/data/batch-secrets-no-override.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/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 new file mode 100644 index 000000000..c59c8d43b --- /dev/null +++ b/backend/tests/helper/helper.ts @@ -0,0 +1,96 @@ +// 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 +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 axiosInstance.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 axiosInstance.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 axiosInstance.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() +} + +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 new file mode 100644 index 000000000..4df6056fe --- /dev/null +++ b/backend/tests/integration-tests/routes/v2/secrets.test.ts @@ -0,0 +1,408 @@ +import request from 'supertest' +import main from '../../../../src/index' +import { testWorkspaceId } from '../../../../src/utils/addDevelopmentUser'; +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'); + +// 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; +}); + +afterAll(async () => { + server.close(); +}); + +describe("GET /api/v2/secrets", () => { + describe("Get secrets via JTW", () => { + test("should create secrets and read secrets via jwt", 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: batchSecretRequestWithNoOverride + }) + + 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() + } + }) + + 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) + + 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(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() + } + }) + }) + + 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 () => { + 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 + }) + + 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(); + }); + } 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 diff --git a/backend/tests/integration-tests/routes/v2/service-tokens.ts b/backend/tests/integration-tests/routes/v2/service-tokens.ts new file mode 100644 index 000000000..b011db36f --- /dev/null +++ b/backend/tests/integration-tests/routes/v2/service-tokens.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 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'", () => {