mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #1408 from akhilmhdh/feat/integration-test-secret-ops
Integration test for secret ops
This commit is contained in:
4
.env.test.example
Normal file
4
.env.test.example
Normal file
@@ -0,0 +1,4 @@
|
||||
REDIS_URL=redis://localhost:6379
|
||||
DB_CONNECTION_URI=postgres://infisical:infisical@localhost/infisical?sslmode=disable
|
||||
AUTH_SECRET=4bnfe4e407b8921c104518903515b218
|
||||
ENCRYPTION_KEY=4bnfe4e407b8921c104518903515b218
|
||||
@@ -5,9 +5,17 @@ on:
|
||||
- "infisical/v*.*.*-postgres"
|
||||
|
||||
jobs:
|
||||
infisical-tests:
|
||||
name: Run tests before deployment
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run backend api integration tests
|
||||
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
|
||||
uses: ./.github/workflows/run-backend-tests.yml
|
||||
infisical-standalone:
|
||||
name: Build infisical standalone image postgres
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-tests]
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
|
||||
47
.github/workflows/run-backend-tests.yml
vendored
Normal file
47
.github/workflows/run-backend-tests.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: "Run backend tests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "backend/**"
|
||||
- "!backend/README.md"
|
||||
- "!backend/.*"
|
||||
- "backend/.eslintrc.js"
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
check-be-pr:
|
||||
name: Run integration test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||
if: ${{ env.ACT }}
|
||||
name: Install `docker-compose` for local simulations
|
||||
with:
|
||||
version: "2.14.2"
|
||||
- name: 🔧 Setup Node 20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
cache-dependency-path: backend/package-lock.json
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
working-directory: backend
|
||||
- name: Start postgres and redis
|
||||
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
|
||||
- name: Start integration test
|
||||
run: npm run test:e2e
|
||||
working-directory: backend
|
||||
env:
|
||||
REDIS_URL: redis://172.17.0.1:6379
|
||||
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
|
||||
AUTH_SECRET: something-random
|
||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||
- name: cleanup
|
||||
run: |
|
||||
docker-compose -f "docker-compose.dev.yml" down
|
||||
@@ -1,2 +1,3 @@
|
||||
vitest-environment-infisical.ts
|
||||
vitest.config.ts
|
||||
vitest.e2e.config.ts
|
||||
|
||||
@@ -21,6 +21,18 @@ module.exports = {
|
||||
tsconfigRootDir: __dirname
|
||||
},
|
||||
root: true,
|
||||
overrides: [
|
||||
{
|
||||
files: ["./e2e-test/**/*"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
||||
|
||||
71
backend/e2e-test/routes/v1/identity.spec.ts
Normal file
71
backend/e2e-test/routes/v1/identity.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { OrgMembershipRole } from "@app/db/schemas";
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
export const createIdentity = async (name: string, role: string) => {
|
||||
const createIdentityRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/identities",
|
||||
body: {
|
||||
name,
|
||||
role,
|
||||
organizationId: seedData1.organization.id
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
expect(createIdentityRes.statusCode).toBe(200);
|
||||
return createIdentityRes.json().identity;
|
||||
};
|
||||
|
||||
export const deleteIdentity = async (id: string) => {
|
||||
const deleteIdentityRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/identities/${id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
expect(deleteIdentityRes.statusCode).toBe(200);
|
||||
return deleteIdentityRes.json().identity;
|
||||
};
|
||||
|
||||
describe("Identity v1", async () => {
|
||||
test("Create identity", async () => {
|
||||
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
|
||||
expect(newIdentity.name).toBe("mac1");
|
||||
expect(newIdentity.authMethod).toBeNull();
|
||||
|
||||
await deleteIdentity(newIdentity.id);
|
||||
});
|
||||
|
||||
test("Update identity", async () => {
|
||||
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
|
||||
expect(newIdentity.name).toBe("mac1");
|
||||
expect(newIdentity.authMethod).toBeNull();
|
||||
|
||||
const updatedIdentity = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v1/identities/${newIdentity.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
name: "updated-mac-1",
|
||||
role: OrgMembershipRole.Member
|
||||
}
|
||||
});
|
||||
|
||||
expect(updatedIdentity.statusCode).toBe(200);
|
||||
expect(updatedIdentity.json().identity.name).toBe("updated-mac-1");
|
||||
|
||||
await deleteIdentity(newIdentity.id);
|
||||
});
|
||||
|
||||
test("Delete Identity", async () => {
|
||||
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
|
||||
|
||||
const deletedIdentity = await deleteIdentity(newIdentity.id);
|
||||
expect(deletedIdentity.name).toBe("mac1");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
describe("Login V1 Router", async () => {
|
||||
// eslint-disable-next-line
|
||||
const client = new jsrp.client();
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project";
|
||||
|
||||
const createProjectEnvironment = async (name: string, slug: string) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
name,
|
||||
slug
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("environment");
|
||||
return payload.environment;
|
||||
};
|
||||
|
||||
const deleteProjectEnvironment = async (envId: string) => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${envId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("environment");
|
||||
return payload.environment;
|
||||
};
|
||||
|
||||
describe("Project Environment Router", async () => {
|
||||
test("Get default environments", async () => {
|
||||
const res = await testServer.inject({
|
||||
@@ -31,24 +65,10 @@ describe("Project Environment Router", async () => {
|
||||
expect(payload.workspace.environments.length).toBe(3);
|
||||
});
|
||||
|
||||
const mockProjectEnv = { name: "temp", slug: "temp", id: "" }; // id will be filled in create op
|
||||
const mockProjectEnv = { name: "temp", slug: "temp" }; // id will be filled in create op
|
||||
test("Create environment", async () => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
name: mockProjectEnv.name,
|
||||
slug: mockProjectEnv.slug
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("environment");
|
||||
expect(payload.environment).toEqual(
|
||||
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||
expect(newEnvironment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: mockProjectEnv.name,
|
||||
@@ -59,14 +79,15 @@ describe("Project Environment Router", async () => {
|
||||
updatedAt: expect.any(String)
|
||||
})
|
||||
);
|
||||
mockProjectEnv.id = payload.environment.id;
|
||||
await deleteProjectEnvironment(newEnvironment.id);
|
||||
});
|
||||
|
||||
test("Update environment", async () => {
|
||||
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||
const updatedName = { name: "temp#2", slug: "temp2" };
|
||||
const res = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${newEnvironment.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
@@ -82,7 +103,7 @@ describe("Project Environment Router", async () => {
|
||||
expect(payload).toHaveProperty("environment");
|
||||
expect(payload.environment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
id: newEnvironment.id,
|
||||
name: updatedName.name,
|
||||
slug: updatedName.slug,
|
||||
projectId: seedData1.project.id,
|
||||
@@ -91,61 +112,21 @@ describe("Project Environment Router", async () => {
|
||||
updatedAt: expect.any(String)
|
||||
})
|
||||
);
|
||||
mockProjectEnv.name = updatedName.name;
|
||||
mockProjectEnv.slug = updatedName.slug;
|
||||
await deleteProjectEnvironment(newEnvironment.id);
|
||||
});
|
||||
|
||||
test("Delete environment", async () => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("environment");
|
||||
expect(payload.environment).toEqual(
|
||||
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||
const deletedProjectEnvironment = await deleteProjectEnvironment(newEnvironment.id);
|
||||
expect(deletedProjectEnvironment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
id: deletedProjectEnvironment.id,
|
||||
name: mockProjectEnv.name,
|
||||
slug: mockProjectEnv.slug,
|
||||
position: 1,
|
||||
position: 4,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// after all these opreations the list of environment should be still same
|
||||
test("Default list of environment", async () => {
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("workspace");
|
||||
// check for default environments
|
||||
expect(payload).toEqual({
|
||||
workspace: expect.objectContaining({
|
||||
name: seedData1.project.name,
|
||||
id: seedData1.project.id,
|
||||
slug: seedData1.project.slug,
|
||||
environments: expect.arrayContaining([
|
||||
expect.objectContaining(DEFAULT_PROJECT_ENVS[0]),
|
||||
expect.objectContaining(DEFAULT_PROJECT_ENVS[1]),
|
||||
expect.objectContaining(DEFAULT_PROJECT_ENVS[2])
|
||||
])
|
||||
})
|
||||
});
|
||||
// ensure only two default environments exist
|
||||
expect(payload.workspace.environments.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
const createFolder = async (dto: { path: string; name: string }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
name: dto.name,
|
||||
path: dto.path
|
||||
}
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
return res.json().folder;
|
||||
};
|
||||
|
||||
const deleteFolder = async (dto: { path: string; id: string }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/folders/${dto.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: dto.path
|
||||
}
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
return res.json().folder;
|
||||
};
|
||||
|
||||
describe("Secret Folder Router", async () => {
|
||||
test.each([
|
||||
{ name: "folder1", path: "/" }, // one in root
|
||||
@@ -7,30 +42,15 @@ describe("Secret Folder Router", async () => {
|
||||
{ name: "folder2", path: "/" },
|
||||
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
|
||||
])("Create folder $name in $path", async ({ name, path }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
name,
|
||||
path
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("folder");
|
||||
const createdFolder = await createFolder({ path, name });
|
||||
// check for default environments
|
||||
expect(payload).toEqual({
|
||||
folder: expect.objectContaining({
|
||||
expect(createdFolder).toEqual(
|
||||
expect.objectContaining({
|
||||
name,
|
||||
id: expect.any(String)
|
||||
})
|
||||
});
|
||||
);
|
||||
await deleteFolder({ path, id: createdFolder.id });
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -43,6 +63,8 @@ describe("Secret Folder Router", async () => {
|
||||
},
|
||||
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
|
||||
])("Get folders $path", async ({ path, expected }) => {
|
||||
const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path })));
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/folders`,
|
||||
@@ -59,36 +81,22 @@ describe("Secret Folder Router", async () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("folders");
|
||||
expect(payload.folders.length).toBe(expected.length);
|
||||
expect(payload).toEqual({ folders: expected.folders.map((el) => expect.objectContaining(el)) });
|
||||
});
|
||||
|
||||
let toBeDeleteFolderId = "";
|
||||
test("Update a deep folder", async () => {
|
||||
const res = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v1/folders/folder1`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
name: "folder-updated",
|
||||
path: "/level1/level2"
|
||||
}
|
||||
expect(payload.folders.length >= expected.folders.length).toBeTruthy();
|
||||
expect(payload).toEqual({
|
||||
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("folder");
|
||||
expect(payload.folder).toEqual(
|
||||
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id })));
|
||||
});
|
||||
|
||||
test("Update a deep folder", async () => {
|
||||
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
|
||||
expect(newFolder).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: "folder-updated"
|
||||
})
|
||||
);
|
||||
toBeDeleteFolderId = payload.folder.id;
|
||||
|
||||
const resUpdatedFolders = await testServer.inject({
|
||||
method: "GET",
|
||||
@@ -106,14 +114,16 @@ describe("Secret Folder Router", async () => {
|
||||
expect(resUpdatedFolders.statusCode).toBe(200);
|
||||
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
|
||||
expect(updatedFolderList).toHaveProperty("folders");
|
||||
expect(updatedFolderList.folders.length).toEqual(1);
|
||||
expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
|
||||
|
||||
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
|
||||
});
|
||||
|
||||
test("Delete a deep folder", async () => {
|
||||
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/folders/${toBeDeleteFolderId}`,
|
||||
url: `/api/v1/folders/${newFolder.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
|
||||
@@ -1,32 +1,57 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
describe("Secret Folder Router", async () => {
|
||||
const createSecretImport = async (importPath: string, importEnv: string) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/secret-imports`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/",
|
||||
import: {
|
||||
environment: importEnv,
|
||||
path: importPath
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("secretImport");
|
||||
return payload.secretImport;
|
||||
};
|
||||
|
||||
const deleteSecretImport = async (id: string) => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/secret-imports/${id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/"
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("secretImport");
|
||||
return payload.secretImport;
|
||||
};
|
||||
|
||||
describe("Secret Import Router", async () => {
|
||||
test.each([
|
||||
{ importEnv: "dev", importPath: "/" }, // one in root
|
||||
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
||||
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/secret-imports`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/",
|
||||
import: {
|
||||
environment: importEnv,
|
||||
path: importPath
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("secretImport");
|
||||
// check for default environments
|
||||
expect(payload.secretImport).toEqual(
|
||||
const payload = await createSecretImport(importPath, importEnv);
|
||||
expect(payload).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
importPath: expect.any(String),
|
||||
@@ -37,10 +62,12 @@ describe("Secret Folder Router", async () => {
|
||||
})
|
||||
})
|
||||
);
|
||||
await deleteSecretImport(payload.id);
|
||||
});
|
||||
|
||||
let testSecretImportId = "";
|
||||
test("Get secret imports", async () => {
|
||||
const createdImport1 = await createSecretImport("/", "dev");
|
||||
const createdImport2 = await createSecretImport("/", "staging");
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/secret-imports`,
|
||||
@@ -58,7 +85,6 @@ describe("Secret Folder Router", async () => {
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("secretImports");
|
||||
expect(payload.secretImports.length).toBe(2);
|
||||
testSecretImportId = payload.secretImports[0].id;
|
||||
expect(payload.secretImports).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -72,12 +98,20 @@ describe("Secret Folder Router", async () => {
|
||||
})
|
||||
])
|
||||
);
|
||||
await deleteSecretImport(createdImport1.id);
|
||||
await deleteSecretImport(createdImport2.id);
|
||||
});
|
||||
|
||||
test("Update secret import position", async () => {
|
||||
const res = await testServer.inject({
|
||||
const devImportDetails = { path: "/", envSlug: "dev" };
|
||||
const stagingImportDetails = { path: "/", envSlug: "staging" };
|
||||
|
||||
const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
|
||||
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
|
||||
|
||||
const updateImportRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v1/secret-imports/${testSecretImportId}`,
|
||||
url: `/api/v1/secret-imports/${createdImport1.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
@@ -91,8 +125,8 @@ describe("Secret Folder Router", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(updateImportRes.statusCode).toBe(200);
|
||||
const payload = JSON.parse(updateImportRes.payload);
|
||||
expect(payload).toHaveProperty("secretImport");
|
||||
// check for default environments
|
||||
expect(payload.secretImport).toEqual(
|
||||
@@ -102,7 +136,7 @@ describe("Secret Folder Router", async () => {
|
||||
position: 2,
|
||||
importEnv: expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
slug: expect.any(String),
|
||||
slug: expect.stringMatching(devImportDetails.envSlug),
|
||||
id: expect.any(String)
|
||||
})
|
||||
})
|
||||
@@ -124,28 +158,19 @@ describe("Secret Folder Router", async () => {
|
||||
expect(secretImportsListRes.statusCode).toBe(200);
|
||||
const secretImportList = JSON.parse(secretImportsListRes.payload);
|
||||
expect(secretImportList).toHaveProperty("secretImports");
|
||||
expect(secretImportList.secretImports[1].id).toEqual(testSecretImportId);
|
||||
expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id);
|
||||
expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id);
|
||||
|
||||
await deleteSecretImport(createdImport1.id);
|
||||
await deleteSecretImport(createdImport2.id);
|
||||
});
|
||||
|
||||
test("Delete secret import position", async () => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/secret-imports/${testSecretImportId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/"
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("secretImport");
|
||||
const createdImport1 = await createSecretImport("/", "dev");
|
||||
const createdImport2 = await createSecretImport("/", "staging");
|
||||
const deletedImport = await deleteSecretImport(createdImport1.id);
|
||||
// check for default environments
|
||||
expect(payload.secretImport).toEqual(
|
||||
expect(deletedImport).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
importPath: expect.any(String),
|
||||
@@ -175,5 +200,7 @@ describe("Secret Folder Router", async () => {
|
||||
expect(secretImportList).toHaveProperty("secretImports");
|
||||
expect(secretImportList.secretImports.length).toEqual(1);
|
||||
expect(secretImportList.secretImports[0].position).toEqual(1);
|
||||
|
||||
await deleteSecretImport(createdImport2.id);
|
||||
});
|
||||
});
|
||||
|
||||
579
backend/e2e-test/routes/v2/service-token.spec.ts
Normal file
579
backend/e2e-test/routes/v2/service-token.spec.ts
Normal file
@@ -0,0 +1,579 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { SecretType, TSecrets } from "@app/db/schemas";
|
||||
import { decryptSecret, encryptSecret, getUserPrivateKey, seedData1 } from "@app/db/seed-data";
|
||||
import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
|
||||
const createServiceToken = async (
|
||||
scopes: { environment: string; secretPath: string }[],
|
||||
permissions: ("read" | "write")[]
|
||||
) => {
|
||||
const projectKeyRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v2/workspace/${seedData1.project.id}/encrypted-key`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
const projectKeyEnc = JSON.parse(projectKeyRes.payload);
|
||||
|
||||
const userInfoRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: "/api/v2/users/me",
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
const { user: userInfo } = JSON.parse(userInfoRes.payload);
|
||||
const privateKey = await getUserPrivateKey(seedData1.password, userInfo);
|
||||
const projectKey = decryptAsymmetric({
|
||||
ciphertext: projectKeyEnc.encryptedKey,
|
||||
nonce: projectKeyEnc.nonce,
|
||||
publicKey: projectKeyEnc.sender.publicKey,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(projectKey, randomBytes);
|
||||
const serviceTokenRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: "/api/v2/service-token",
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
name: "test-token",
|
||||
workspaceId: seedData1.project.id,
|
||||
scopes,
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
permissions,
|
||||
expiresIn: null
|
||||
}
|
||||
});
|
||||
expect(serviceTokenRes.statusCode).toBe(200);
|
||||
const serviceTokenInfo = serviceTokenRes.json();
|
||||
expect(serviceTokenInfo).toHaveProperty("serviceToken");
|
||||
expect(serviceTokenInfo).toHaveProperty("serviceTokenData");
|
||||
return `${serviceTokenInfo.serviceToken}.${randomBytes}`;
|
||||
};
|
||||
|
||||
const deleteServiceToken = async () => {
|
||||
const serviceTokenListRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
expect(serviceTokenListRes.statusCode).toBe(200);
|
||||
const serviceTokens = JSON.parse(serviceTokenListRes.payload).serviceTokenData as { name: string; id: string }[];
|
||||
expect(serviceTokens.length).toBeGreaterThan(0);
|
||||
const serviceTokenInfo = serviceTokens.find(({ name }) => name === "test-token");
|
||||
expect(serviceTokenInfo).toBeDefined();
|
||||
|
||||
const deleteTokenRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v2/service-token/${serviceTokenInfo?.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
expect(deleteTokenRes.statusCode).toBe(200);
|
||||
};
|
||||
|
||||
const createSecret = async (dto: {
|
||||
projectKey: string;
|
||||
path: string;
|
||||
key: string;
|
||||
value: string;
|
||||
comment: string;
|
||||
type?: SecretType;
|
||||
token: string;
|
||||
}) => {
|
||||
const createSecretReqBody = {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: dto.type || SecretType.Shared,
|
||||
secretPath: dto.path,
|
||||
...encryptSecret(dto.projectKey, dto.key, dto.value, dto.comment)
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/${dto.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${dto.token}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
expect(createSecRes.statusCode).toBe(200);
|
||||
const createdSecretPayload = JSON.parse(createSecRes.payload);
|
||||
expect(createdSecretPayload).toHaveProperty("secret");
|
||||
return createdSecretPayload.secret;
|
||||
};
|
||||
|
||||
const deleteSecret = async (dto: { path: string; key: string; token: string }) => {
|
||||
const deleteSecRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v3/secrets/${dto.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${dto.token}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: dto.path
|
||||
}
|
||||
});
|
||||
expect(deleteSecRes.statusCode).toBe(200);
|
||||
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
|
||||
expect(updatedSecretPayload).toHaveProperty("secret");
|
||||
return updatedSecretPayload.secret;
|
||||
};
|
||||
|
||||
describe("Service token secret ops", async () => {
|
||||
let serviceToken = "";
|
||||
let projectKey = "";
|
||||
let folderId = "";
|
||||
beforeAll(async () => {
|
||||
serviceToken = await createServiceToken(
|
||||
[{ secretPath: "/**", environment: seedData1.environment.slug }],
|
||||
["read", "write"]
|
||||
);
|
||||
|
||||
// this is ensure cli service token decryptiong working fine
|
||||
const serviceTokenInfoRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: "/api/v2/service-token",
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(serviceTokenInfoRes.statusCode).toBe(200);
|
||||
const serviceTokenInfo = serviceTokenInfoRes.json();
|
||||
const serviceTokenParts = serviceToken.split(".");
|
||||
projectKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
key: serviceTokenParts[3],
|
||||
tag: serviceTokenInfo.tag,
|
||||
ciphertext: serviceTokenInfo.encryptedKey,
|
||||
iv: serviceTokenInfo.iv
|
||||
});
|
||||
|
||||
// create a deep folder
|
||||
const folderCreate = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
name: "folder",
|
||||
path: "/nested1/nested2"
|
||||
}
|
||||
});
|
||||
expect(folderCreate.statusCode).toBe(200);
|
||||
folderId = folderCreate.json().folder.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteServiceToken();
|
||||
|
||||
// create a deep folder
|
||||
const deleteFolder = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/folders/${folderId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/nested1/nested2"
|
||||
}
|
||||
});
|
||||
expect(deleteFolder.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
const testSecrets = [
|
||||
{
|
||||
path: "/",
|
||||
secret: {
|
||||
key: "ST-SEC",
|
||||
value: "something-secret",
|
||||
comment: "some comment"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/nested1/nested2/folder",
|
||||
secret: {
|
||||
key: "NESTED-ST-SEC",
|
||||
value: "something-secret",
|
||||
comment: "some comment"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const getSecrets = async (environment: string, secretPath = "/") => {
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v3/secrets`,
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
query: {
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: seedData1.project.id
|
||||
}
|
||||
});
|
||||
const secrets: TSecrets[] = JSON.parse(res.payload).secrets || [];
|
||||
return secrets.map((el) => ({ ...decryptSecret(projectKey, el), type: el.type }));
|
||||
};
|
||||
|
||||
test.each(testSecrets)("Create secret in path $path", async ({ secret, path }) => {
|
||||
const createdSecret = await createSecret({ projectKey, path, ...secret, token: serviceToken });
|
||||
const decryptedSecret = decryptSecret(projectKey, createdSecret);
|
||||
expect(decryptedSecret.key).toEqual(secret.key);
|
||||
expect(decryptedSecret.value).toEqual(secret.value);
|
||||
expect(decryptedSecret.comment).toEqual(secret.comment);
|
||||
expect(decryptedSecret.version).toEqual(1);
|
||||
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: secret.key,
|
||||
value: secret.value,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
await deleteSecret({ path, key: secret.key, token: serviceToken });
|
||||
});
|
||||
|
||||
test.each(testSecrets)("Get secret by name in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ projectKey, path, ...secret, token: serviceToken });
|
||||
|
||||
const getSecByNameRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v3/secrets/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
query: {
|
||||
secretPath: path,
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug
|
||||
}
|
||||
});
|
||||
expect(getSecByNameRes.statusCode).toBe(200);
|
||||
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
|
||||
expect(getSecretByNamePayload).toHaveProperty("secret");
|
||||
const decryptedSecret = decryptSecret(projectKey, getSecretByNamePayload.secret);
|
||||
expect(decryptedSecret.key).toEqual(secret.key);
|
||||
expect(decryptedSecret.value).toEqual(secret.value);
|
||||
expect(decryptedSecret.comment).toEqual(secret.comment);
|
||||
|
||||
await deleteSecret({ path, key: secret.key, token: serviceToken });
|
||||
});
|
||||
|
||||
test.each(testSecrets)("Update secret in path $path", async ({ path, secret }) => {
|
||||
await createSecret({ projectKey, path, ...secret, token: serviceToken });
|
||||
const updateSecretReqBody = {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Shared,
|
||||
secretPath: path,
|
||||
...encryptSecret(projectKey, secret.key, "new-value", secret.comment)
|
||||
};
|
||||
const updateSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v3/secrets/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
body: updateSecretReqBody
|
||||
});
|
||||
expect(updateSecRes.statusCode).toBe(200);
|
||||
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
|
||||
expect(updatedSecretPayload).toHaveProperty("secret");
|
||||
const decryptedSecret = decryptSecret(projectKey, updatedSecretPayload.secret);
|
||||
expect(decryptedSecret.key).toEqual(secret.key);
|
||||
expect(decryptedSecret.value).toEqual("new-value");
|
||||
expect(decryptedSecret.comment).toEqual(secret.comment);
|
||||
|
||||
// list secret should have updated value
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: secret.key,
|
||||
value: "new-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await deleteSecret({ path, key: secret.key, token: serviceToken });
|
||||
});
|
||||
|
||||
test.each(testSecrets)("Delete secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ projectKey, path, ...secret, token: serviceToken });
|
||||
const deletedSecret = await deleteSecret({ path, key: secret.key, token: serviceToken });
|
||||
const decryptedSecret = decryptSecret(projectKey, deletedSecret);
|
||||
expect(decryptedSecret.key).toEqual(secret.key);
|
||||
|
||||
// shared secret deletion should delete personal ones also
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: secret.key,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test.each(testSecrets)("Bulk create secrets in path $path", async ({ secret, path }) => {
|
||||
const createSharedSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretName: `BULK-${secret.key}-${i + 1}`,
|
||||
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(createSharedSecRes.statusCode).toBe(200);
|
||||
const createSharedSecPayload = JSON.parse(createSharedSecRes.payload);
|
||||
expect(createSharedSecPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
expect.objectContaining({
|
||||
key: `BULK-${secret.key}-${i + 1}`,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
deleteSecret({ path, token: serviceToken, key: `BULK-${secret.key}-${i + 1}` })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test.each(testSecrets)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-1`, path, token: serviceToken });
|
||||
|
||||
const createSharedSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretName: `BULK-${secret.key}-${i + 1}`,
|
||||
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(createSharedSecRes.statusCode).toBe(400);
|
||||
|
||||
await deleteSecret({ path, key: `BULK-${secret.key}-1`, token: serviceToken });
|
||||
});
|
||||
|
||||
test.each(testSecrets)("Bulk update secrets in path $path", async ({ secret, path }) => {
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
|
||||
)
|
||||
);
|
||||
|
||||
const updateSharedSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v3/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretName: `BULK-${secret.key}-${i + 1}`,
|
||||
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, "update-value", secret.comment)
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(updateSharedSecRes.statusCode).toBe(200);
|
||||
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
|
||||
expect(updateSharedSecPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
expect.objectContaining({
|
||||
key: `BULK-${secret.key}-${i + 1}`,
|
||||
value: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}`, token: serviceToken })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test.each(testSecrets)("Bulk delete secrets in path $path", async ({ secret, path }) => {
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
|
||||
)
|
||||
);
|
||||
|
||||
const deletedSharedSecRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v3/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretName: `BULK-${secret.key}-${i + 1}`
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
expect(deletedSharedSecRes.statusCode).toBe(200);
|
||||
const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload);
|
||||
expect(deletedSecretPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.not.arrayContaining(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
expect.objectContaining({
|
||||
key: `BULK-${secret.value}-${i + 1}`,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Service token fail cases", async () => {
|
||||
test("Unauthorized secret path access", async () => {
|
||||
const serviceToken = await createServiceToken(
|
||||
[{ secretPath: "/", environment: seedData1.environment.slug }],
|
||||
["read", "write"]
|
||||
);
|
||||
const fetchSecrets = await testServer.inject({
|
||||
method: "GET",
|
||||
url: "/api/v3/secrets",
|
||||
query: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: "/nested/deep"
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(fetchSecrets.statusCode).toBe(401);
|
||||
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
||||
await deleteServiceToken();
|
||||
});
|
||||
|
||||
test("Unauthorized secret environment access", async () => {
|
||||
const serviceToken = await createServiceToken(
|
||||
[{ secretPath: "/", environment: seedData1.environment.slug }],
|
||||
["read", "write"]
|
||||
);
|
||||
const fetchSecrets = await testServer.inject({
|
||||
method: "GET",
|
||||
url: "/api/v3/secrets",
|
||||
query: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: "prod",
|
||||
secretPath: "/"
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(fetchSecrets.statusCode).toBe(401);
|
||||
expect(fetchSecrets.json().error).toBe("PermissionDenied");
|
||||
await deleteServiceToken();
|
||||
});
|
||||
|
||||
test("Unauthorized write operation", async () => {
|
||||
const serviceToken = await createServiceToken(
|
||||
[{ secretPath: "/", environment: seedData1.environment.slug }],
|
||||
["read"]
|
||||
);
|
||||
const writeSecrets = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/NEW`,
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Shared,
|
||||
secretPath: "/",
|
||||
// doesn't matter project key because this will fail before that due to read only access
|
||||
...encryptSecret(crypto.randomBytes(16).toString("hex"), "NEW", "value", "")
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(writeSecrets.statusCode).toBe(401);
|
||||
expect(writeSecrets.json().error).toBe("PermissionDenied");
|
||||
|
||||
// but read access should still work fine
|
||||
const fetchSecrets = await testServer.inject({
|
||||
method: "GET",
|
||||
url: "/api/v3/secrets",
|
||||
query: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: "/"
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${serviceToken}`
|
||||
}
|
||||
});
|
||||
expect(fetchSecrets.statusCode).toBe(200);
|
||||
await deleteServiceToken();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,30 @@
|
||||
// import { main } from "@app/server/app";
|
||||
import { initEnvConfig } from "@app/lib/config/env";
|
||||
// eslint-disable-next-line
|
||||
import "ts-node/register";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
import jwt from "jsonwebtoken";
|
||||
import knex from "knex";
|
||||
import path from "path";
|
||||
import { mockSmtpServer } from "./mocks/smtp";
|
||||
import { initLogger } from "@app/lib/logger";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import "ts-node/register";
|
||||
import { main } from "@app/server/app";
|
||||
import { mockQueue } from "./mocks/queue";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { initEnvConfig } from "@app/lib/config/env";
|
||||
import { initLogger } from "@app/lib/logger";
|
||||
import { main } from "@app/server/app";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, "../.env.test") });
|
||||
import { mockQueue } from "./mocks/queue";
|
||||
import { mockSmtpServer } from "./mocks/smtp";
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
||||
export default {
|
||||
name: "knex-env",
|
||||
transformMode: "ssr",
|
||||
async setup() {
|
||||
const logger = await initLogger();
|
||||
const cfg = initEnvConfig(logger);
|
||||
const db = knex({
|
||||
client: "pg",
|
||||
connection: process.env.DB_CONNECTION_URI,
|
||||
connection: cfg.DB_CONNECTION_URI,
|
||||
migrations: {
|
||||
directory: path.join(__dirname, "../src/db/migrations"),
|
||||
extension: "ts",
|
||||
@@ -37,8 +41,6 @@ export default {
|
||||
await db.seed.run();
|
||||
const smtp = mockSmtpServer();
|
||||
const queue = mockQueue();
|
||||
const logger = await initLogger();
|
||||
const cfg = initEnvConfig(logger);
|
||||
const server = await main({ db, smtp, logger, queue });
|
||||
// @ts-expect-error type
|
||||
globalThis.testServer = server;
|
||||
|
||||
112
backend/package-lock.json
generated
112
backend/package-lock.json
generated
@@ -102,7 +102,7 @@
|
||||
"tsx": "^4.4.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite-tsconfig-paths": "^4.2.2",
|
||||
"vitest": "^1.0.4"
|
||||
"vitest": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -3881,6 +3881,12 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||
@@ -4543,13 +4549,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.0.4.tgz",
|
||||
"integrity": "sha512-/NRN9N88qjg3dkhmFcCBwhn/Ie4h064pY3iv7WLRsDJW7dXnEgeoa8W9zy7gIPluhz6CkgqiB3HmpIXgmEY5dQ==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
|
||||
"integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/spy": "1.0.4",
|
||||
"@vitest/utils": "1.0.4",
|
||||
"@vitest/spy": "1.2.2",
|
||||
"@vitest/utils": "1.2.2",
|
||||
"chai": "^4.3.10"
|
||||
},
|
||||
"funding": {
|
||||
@@ -4557,12 +4563,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.0.4.tgz",
|
||||
"integrity": "sha512-rhOQ9FZTEkV41JWXozFM8YgOqaG9zA7QXbhg5gy6mFOVqh4PcupirIJ+wN7QjeJt8S8nJRYuZH1OjJjsbxAXTQ==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
|
||||
"integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "1.0.4",
|
||||
"@vitest/utils": "1.2.2",
|
||||
"p-limit": "^5.0.0",
|
||||
"pathe": "^1.1.1"
|
||||
},
|
||||
@@ -4598,9 +4604,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.0.4.tgz",
|
||||
"integrity": "sha512-vkfXUrNyNRA/Gzsp2lpyJxh94vU2OHT1amoD6WuvUAA12n32xeVZQ0KjjQIf8F6u7bcq2A2k969fMVxEsxeKYA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
|
||||
"integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -4612,9 +4618,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.0.4.tgz",
|
||||
"integrity": "sha512-9ojTFRL1AJVh0hvfzAQpm0QS6xIS+1HFIw94kl/1ucTfGCaj1LV/iuJU4Y6cdR03EzPDygxTHwE1JOm+5RCcvA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
|
||||
"integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tinyspy": "^2.2.0"
|
||||
@@ -4624,12 +4630,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.0.4.tgz",
|
||||
"integrity": "sha512-gsswWDXxtt0QvtK/y/LWukN7sGMYmnCcv1qv05CsY6cU/Y1zpGX1QuvLs+GO1inczpE6Owixeel3ShkjhYtGfA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
|
||||
"integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"diff-sequences": "^29.6.3",
|
||||
"estree-walker": "^3.0.3",
|
||||
"loupe": "^2.3.7",
|
||||
"pretty-format": "^29.7.0"
|
||||
},
|
||||
@@ -4700,9 +4707,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz",
|
||||
"integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==",
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
|
||||
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@@ -5532,9 +5539,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "4.3.10",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
|
||||
"integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
|
||||
"integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"assertion-error": "^1.1.0",
|
||||
@@ -6728,6 +6735,15 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
@@ -8800,9 +8816,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
|
||||
"version": "0.30.7",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
|
||||
"integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
@@ -11622,18 +11638,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz",
|
||||
"integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==",
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
|
||||
"integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
|
||||
"integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
|
||||
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -12766,9 +12782,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.0.4.tgz",
|
||||
"integrity": "sha512-9xQQtHdsz5Qn8hqbV7UKqkm8YkJhzT/zr41Dmt5N7AlD8hJXw/Z7y0QiD5I8lnTthV9Rvcvi0QW7PI0Fq83ZPg==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
|
||||
"integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
@@ -13242,17 +13258,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.0.4.tgz",
|
||||
"integrity": "sha512-s1GQHp/UOeWEo4+aXDOeFBJwFzL6mjycbQwwKWX2QcYfh/7tIerS59hWQ20mxzupTJluA2SdwiBuWwQHH67ckg==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
|
||||
"integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "1.0.4",
|
||||
"@vitest/runner": "1.0.4",
|
||||
"@vitest/snapshot": "1.0.4",
|
||||
"@vitest/spy": "1.0.4",
|
||||
"@vitest/utils": "1.0.4",
|
||||
"acorn-walk": "^8.3.0",
|
||||
"@vitest/expect": "1.2.2",
|
||||
"@vitest/runner": "1.2.2",
|
||||
"@vitest/snapshot": "1.2.2",
|
||||
"@vitest/spy": "1.2.2",
|
||||
"@vitest/utils": "1.2.2",
|
||||
"acorn-walk": "^8.3.2",
|
||||
"cac": "^6.7.14",
|
||||
"chai": "^4.3.10",
|
||||
"debug": "^4.3.4",
|
||||
@@ -13264,9 +13280,9 @@
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^1.3.0",
|
||||
"tinybench": "^2.5.1",
|
||||
"tinypool": "^0.8.1",
|
||||
"tinypool": "^0.8.2",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "1.0.4",
|
||||
"vite-node": "1.2.2",
|
||||
"why-is-node-running": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||
"seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run"
|
||||
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -67,7 +67,7 @@
|
||||
"tsx": "^4.4.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite-tsconfig-paths": "^4.2.2",
|
||||
"vitest": "^1.0.4"
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.485.0",
|
||||
@@ -126,4 +126,4 @@
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ import promptSync from "prompt-sync";
|
||||
const prompt = promptSync({ sigint: true });
|
||||
|
||||
const migrationName = prompt("Enter name for seedfile: ");
|
||||
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1;
|
||||
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seeds")).length || 1;
|
||||
execSync(
|
||||
`npx knex seed:make --knexfile ${path.join(
|
||||
__dirname,
|
||||
"../src/db/knexfile.ts"
|
||||
)} -x ts ${fileCounter}-${migrationName}`,
|
||||
`npx knex seed:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${
|
||||
fileCounter + 1
|
||||
}-${migrationName}`,
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
|
||||
@@ -10,6 +10,10 @@ dotenv.config({
|
||||
path: path.join(__dirname, "../../../.env.migration"),
|
||||
debug: true
|
||||
});
|
||||
dotenv.config({
|
||||
path: path.join(__dirname, "../../../.env"),
|
||||
debug: true
|
||||
});
|
||||
export default {
|
||||
development: {
|
||||
client: "postgres",
|
||||
|
||||
@@ -6,13 +6,14 @@ import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import {
|
||||
decryptAsymmetric,
|
||||
// decryptAsymmetric,
|
||||
decryptSymmetric,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric
|
||||
encryptSymmetric128BitHexKeyUTF8
|
||||
} from "@app/lib/crypto";
|
||||
|
||||
import { TUserEncryptionKeys } from "./schemas";
|
||||
import { TSecrets, TUserEncryptionKeys } from "./schemas";
|
||||
|
||||
export const seedData1 = {
|
||||
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
||||
@@ -31,6 +32,14 @@ export const seedData1 = {
|
||||
name: "Development",
|
||||
slug: "dev"
|
||||
},
|
||||
machineIdentity: {
|
||||
id: "88fa7aed-9288-401e-a4c9-fa9430be62a0",
|
||||
name: "mac1",
|
||||
clientCredentials: {
|
||||
id: "3f6135db-f237-421d-af66-a8f4e80d443b",
|
||||
secret: "da35a5a5a7b57f977a9a73394506e878a7175d06606df43dc93e1472b10cf339"
|
||||
}
|
||||
},
|
||||
token: {
|
||||
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
||||
}
|
||||
@@ -73,7 +82,7 @@ export const generateUserSrpKeys = async (password: string) => {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = encryptSymmetric(privateKey, key.toString("base64"));
|
||||
} = encryptSymmetric128BitHexKeyUTF8(privateKey, key);
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
@@ -81,7 +90,7 @@ export const generateUserSrpKeys = async (password: string) => {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
|
||||
} = encryptSymmetric128BitHexKeyUTF8(key.toString("hex"), derivedKey);
|
||||
|
||||
return {
|
||||
protectedKey,
|
||||
@@ -107,32 +116,102 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
|
||||
raw: true
|
||||
});
|
||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||
const key = decryptSymmetric({
|
||||
|
||||
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: user.protectedKey as string,
|
||||
iv: user.protectedKeyIV as string,
|
||||
tag: user.protectedKeyTag as string,
|
||||
key: derivedKey.toString("base64")
|
||||
key: derivedKey
|
||||
});
|
||||
const privateKey = decryptSymmetric({
|
||||
|
||||
const privateKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag,
|
||||
key
|
||||
key: Buffer.from(key, "hex")
|
||||
});
|
||||
return privateKey;
|
||||
};
|
||||
|
||||
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
|
||||
export const buildUserProjectKey = (privateKey: string, publickey: string) => {
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
|
||||
return { nonce, ciphertext };
|
||||
};
|
||||
|
||||
// export const getUserProjectKey = async (privateKey: string) => {
|
||||
// const key = decryptAsymmetric({
|
||||
// ciphertext: decryptFileKey.encryptedKey,
|
||||
// nonce: decryptFileKey.nonce,
|
||||
// publicKey: decryptFileKey.sender.publicKey,
|
||||
// privateKey: PRIVATE_KEY
|
||||
// });
|
||||
// };
|
||||
export const getUserProjectKey = async (privateKey: string, ciphertext: string, nonce: string, publicKey: string) => {
|
||||
return decryptAsymmetric({
|
||||
ciphertext,
|
||||
nonce,
|
||||
publicKey,
|
||||
privateKey
|
||||
});
|
||||
};
|
||||
|
||||
export const encryptSecret = (encKey: string, key: string, value?: string, comment?: string) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag
|
||||
} = encryptSymmetric128BitHexKeyUTF8(key, encKey);
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag
|
||||
} = encryptSymmetric128BitHexKeyUTF8(value ?? "", encKey);
|
||||
|
||||
// encrypt comment
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag
|
||||
} = encryptSymmetric128BitHexKeyUTF8(comment ?? "", encKey);
|
||||
|
||||
return {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
};
|
||||
};
|
||||
|
||||
export const decryptSecret = (decryptKey: string, encSecret: TSecrets) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
key: decryptKey,
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
tag: encSecret.secretKeyTag,
|
||||
iv: encSecret.secretKeyIV
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
key: decryptKey,
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
tag: encSecret.secretValueTag,
|
||||
iv: encSecret.secretValueIV
|
||||
});
|
||||
|
||||
const secretComment =
|
||||
encSecret.secretCommentIV && encSecret.secretCommentTag && encSecret.secretCommentCiphertext
|
||||
? decryptSymmetric128BitHexKeyUTF8({
|
||||
key: decryptKey,
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
tag: encSecret.secretCommentTag,
|
||||
iv: encSecret.secretCommentIV
|
||||
})
|
||||
: "";
|
||||
|
||||
return {
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
comment: secretComment,
|
||||
version: encSecret.version
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
const [user] = await knex(TableName.Users)
|
||||
.insert([
|
||||
{
|
||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.id,
|
||||
email: seedData1.email,
|
||||
superAdmin: true,
|
||||
@@ -48,7 +49,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
]);
|
||||
|
||||
await knex(TableName.AuthTokenSession).insert({
|
||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.token.id,
|
||||
userId: seedData1.id,
|
||||
ip: "151.196.220.213",
|
||||
|
||||
@@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
const [org] = await knex(TableName.Organization)
|
||||
.insert([
|
||||
{
|
||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.organization.id,
|
||||
name: "infisical",
|
||||
slug: "infisical",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, TableName } from "../schemas";
|
||||
import { seedData1 } from "../seed-data";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
|
||||
import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
||||
|
||||
export const DEFAULT_PROJECT_ENVS = [
|
||||
{ name: "Development", slug: "dev" },
|
||||
@@ -20,21 +24,32 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
name: seedData1.project.name,
|
||||
orgId: seedData1.organization.id,
|
||||
slug: "first-project",
|
||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.project.id
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
// await knex(TableName.ProjectKeys).insert({
|
||||
// projectId: project.id,
|
||||
// senderId: seedData1.id
|
||||
// });
|
||||
|
||||
await knex(TableName.ProjectMembership).insert({
|
||||
projectId: project.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
userId: seedData1.id
|
||||
});
|
||||
|
||||
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
|
||||
if (!user) throw new Error("User not found");
|
||||
|
||||
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
|
||||
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
|
||||
await knex(TableName.ProjectKeys).insert({
|
||||
projectId: project.id,
|
||||
nonce: projectKey.nonce,
|
||||
encryptedKey: projectKey.ciphertext,
|
||||
receiverId: seedData1.id,
|
||||
senderId: seedData1.id
|
||||
});
|
||||
|
||||
// create default environments and default folders
|
||||
const envs = await knex(TableName.Environment)
|
||||
.insert(
|
||||
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
|
||||
@@ -46,4 +61,19 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
)
|
||||
.returning("*");
|
||||
await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null })));
|
||||
|
||||
// save secret secret blind index
|
||||
const encKey = process.env.ENCRYPTION_KEY;
|
||||
if (!encKey) throw new Error("Missing ENCRYPTION_KEY");
|
||||
const salt = crypto.randomBytes(16).toString("base64");
|
||||
const secretBlindIndex = encryptSymmetric128BitHexKeyUTF8(salt, encKey);
|
||||
// insert secret blind index for project
|
||||
await knex(TableName.SecretBlindIndex).insert({
|
||||
projectId: project.id,
|
||||
encryptedSaltCipherText: secretBlindIndex.ciphertext,
|
||||
saltIV: secretBlindIndex.iv,
|
||||
saltTag: secretBlindIndex.tag,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
});
|
||||
}
|
||||
|
||||
83
backend/src/db/seeds/4-machine-identity.ts
Normal file
83
backend/src/db/seeds/4-machine-identity.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
|
||||
import { seedData1 } from "../seed-data";
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
// Deletes ALL existing entries
|
||||
await knex(TableName.Identity).del();
|
||||
await knex(TableName.IdentityOrgMembership).del();
|
||||
|
||||
// Inserts seed entries
|
||||
await knex(TableName.Identity).insert([
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.machineIdentity.id,
|
||||
name: seedData1.machineIdentity.name,
|
||||
authMethod: IdentityAuthMethod.Univeral
|
||||
}
|
||||
]);
|
||||
const identityUa = await knex(TableName.IdentityUniversalAuth)
|
||||
.insert([
|
||||
{
|
||||
identityId: seedData1.machineIdentity.id,
|
||||
clientId: seedData1.machineIdentity.clientCredentials.id,
|
||||
clientSecretTrustedIps: JSON.stringify([
|
||||
{
|
||||
type: "ipv4",
|
||||
prefix: 0,
|
||||
ipAddress: "0.0.0.0"
|
||||
},
|
||||
{
|
||||
type: "ipv6",
|
||||
prefix: 0,
|
||||
ipAddress: "::"
|
||||
}
|
||||
]),
|
||||
accessTokenTrustedIps: JSON.stringify([
|
||||
{
|
||||
type: "ipv4",
|
||||
prefix: 0,
|
||||
ipAddress: "0.0.0.0"
|
||||
},
|
||||
{
|
||||
type: "ipv6",
|
||||
prefix: 0,
|
||||
ipAddress: "::"
|
||||
}
|
||||
]),
|
||||
accessTokenTTL: 2592000,
|
||||
accessTokenMaxTTL: 2592000,
|
||||
accessTokenNumUsesLimit: 0
|
||||
}
|
||||
])
|
||||
.returning("*");
|
||||
const clientSecretHash = await bcrypt.hash(seedData1.machineIdentity.clientCredentials.secret, 10);
|
||||
await knex(TableName.IdentityUaClientSecret).insert([
|
||||
{
|
||||
identityUAId: identityUa[0].id,
|
||||
description: "",
|
||||
clientSecretTTL: 0,
|
||||
clientSecretNumUses: 0,
|
||||
clientSecretNumUsesLimit: 0,
|
||||
clientSecretPrefix: seedData1.machineIdentity.clientCredentials.secret.slice(0, 4),
|
||||
clientSecretHash,
|
||||
isClientSecretRevoked: false
|
||||
}
|
||||
]);
|
||||
await knex(TableName.IdentityOrgMembership).insert([
|
||||
{
|
||||
identityId: seedData1.machineIdentity.id,
|
||||
orgId: seedData1.organization.id,
|
||||
role: OrgMembershipRole.Admin
|
||||
}
|
||||
]);
|
||||
|
||||
await knex(TableName.IdentityProjectMembership).insert({
|
||||
identityId: seedData1.machineIdentity.id,
|
||||
role: ProjectMembershipRole.Admin,
|
||||
projectId: seedData1.project.id
|
||||
});
|
||||
}
|
||||
27
backend/src/ee/services/license/__mocks__/licence-fns.ts
Normal file
27
backend/src/ee/services/license/__mocks__/licence-fns.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const getDefaultOnPremFeatures = () => {
|
||||
return {
|
||||
_id: null,
|
||||
slug: null,
|
||||
tier: -1,
|
||||
workspaceLimit: null,
|
||||
workspacesUsed: 0,
|
||||
memberLimit: null,
|
||||
membersUsed: 0,
|
||||
environmentLimit: null,
|
||||
environmentsUsed: 0,
|
||||
secretVersioning: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: true,
|
||||
rbac: false,
|
||||
customRateLimits: false,
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
samlSSO: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
secretApproval: false,
|
||||
secretRotation: true
|
||||
};
|
||||
};
|
||||
@@ -44,7 +44,7 @@ export const encryptSymmetric = (plaintext: string, key: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => {
|
||||
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string | Buffer) => {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
|
||||
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
|
||||
|
||||
@@ -58,7 +58,12 @@ export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string)
|
||||
};
|
||||
};
|
||||
|
||||
export const decryptSymmetric128BitHexKeyUTF8 = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => {
|
||||
export const decryptSymmetric128BitHexKeyUTF8 = ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key
|
||||
}: Omit<TDecryptSymmetricInput, "key"> & { key: string | Buffer }): string => {
|
||||
const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64"));
|
||||
|
||||
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||
|
||||
@@ -37,7 +37,7 @@ type TMain = {
|
||||
export const main = async ({ db, smtp, logger, queue }: TMain) => {
|
||||
const appCfg = getConfig();
|
||||
const server = fasitfy({
|
||||
logger,
|
||||
logger: appCfg.NODE_ENV === "test" ? false : logger,
|
||||
trustProxy: true,
|
||||
connectionTimeout: 30 * 1000,
|
||||
ignoreTrailingSlash: true
|
||||
|
||||
@@ -1092,7 +1092,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secrets: z
|
||||
.object({
|
||||
secretName: z.string().trim(),
|
||||
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
||||
secretKeyCiphertext: z.string().trim(),
|
||||
secretKeyIV: z.string().trim(),
|
||||
secretKeyTag: z.string().trim(),
|
||||
@@ -1139,7 +1138,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
projectId,
|
||||
policy,
|
||||
data: {
|
||||
[CommitType.Create]: inputSecrets.filter(({ type }) => type === "shared")
|
||||
[CommitType.Create]: inputSecrets
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
type: el.type,
|
||||
...(el.type === SecretType.Personal ? { userId } : {})
|
||||
});
|
||||
if (el.type === SecretType.Shared) {
|
||||
void bd.orWhere({
|
||||
secretBlindIndex: el.blindIndex,
|
||||
type: SecretType.Personal
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.delete()
|
||||
|
||||
@@ -4,12 +4,16 @@ import { defineConfig } from "vitest/config";
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
env: {
|
||||
NODE_ENV: "test"
|
||||
},
|
||||
environment: "./e2e-test/vitest-environment-knex.ts",
|
||||
include: ["./e2e-test/**/*.spec.ts"],
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
useAtomics: true
|
||||
useAtomics: true,
|
||||
isolate: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user