mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'heads/main' into ENG-3630
This commit is contained in:
17
.github/workflows/nightly-tag-generation.yml
vendored
17
.github/workflows/nightly-tag-generation.yml
vendored
@@ -36,11 +36,23 @@ jobs:
|
||||
|
||||
echo "Latest production tag: $LATEST_STABLE_TAG"
|
||||
|
||||
# Extract version numbers and increment minor version
|
||||
VERSION_NUMBERS=$(echo "$LATEST_STABLE_TAG" | sed 's/^v//')
|
||||
MAJOR=$(echo "$VERSION_NUMBERS" | cut -d'.' -f1)
|
||||
MINOR=$(echo "$VERSION_NUMBERS" | cut -d'.' -f2)
|
||||
PATCH=$(echo "$VERSION_NUMBERS" | cut -d'.' -f3)
|
||||
|
||||
# Increment minor version, reset patch to 0
|
||||
NEXT_MINOR=$((MINOR + 1))
|
||||
NEXT_VERSION="v${MAJOR}.${NEXT_MINOR}.0"
|
||||
|
||||
echo "Next version for nightly: $NEXT_VERSION"
|
||||
|
||||
# Get current date in YYYYMMDD format
|
||||
DATE=$(date +%Y%m%d)
|
||||
|
||||
# Base nightly tag name
|
||||
BASE_TAG="${LATEST_STABLE_TAG}-nightly-${DATE}"
|
||||
# Base nightly tag name using next version
|
||||
BASE_TAG="${NEXT_VERSION}-nightly-${DATE}"
|
||||
|
||||
# Check if this exact tag already exists
|
||||
if git tag --list | grep -q "^${BASE_TAG}$"; then
|
||||
@@ -65,7 +77,6 @@ jobs:
|
||||
|
||||
echo "Generated nightly tag: $NIGHTLY_TAG"
|
||||
echo "NIGHTLY_TAG=$NIGHTLY_TAG" >> $GITHUB_ENV
|
||||
echo "LATEST_PRODUCTION_TAG=$LATEST_STABLE_TAG" >> $GITHUB_ENV
|
||||
|
||||
git tag "$NIGHTLY_TAG"
|
||||
git push origin "$NIGHTLY_TAG"
|
||||
|
||||
@@ -56,6 +56,15 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
incrementBy: async () => {
|
||||
return 1;
|
||||
},
|
||||
pgGetIntItem: async (key) => {
|
||||
const value = store[key];
|
||||
if (typeof value === "number") {
|
||||
return Number(value);
|
||||
}
|
||||
},
|
||||
pgIncrementBy: async () => {
|
||||
return 1;
|
||||
},
|
||||
getItems: async (keys) => {
|
||||
const values = keys.map((key) => {
|
||||
const value = store[key];
|
||||
|
||||
165
backend/e2e-test/routes/v2/secret-folder.spec.ts
Normal file
165
backend/e2e-test/routes/v2/secret-folder.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
const createFolder = async (dto: { path: string; name: string }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v2/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: 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/v2/folders/${dto.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: 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
|
||||
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
|
||||
{ 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 createdFolder = await createFolder({ path, name });
|
||||
// check for default environments
|
||||
expect(createdFolder).toEqual(
|
||||
expect.objectContaining({
|
||||
name,
|
||||
id: expect.any(String)
|
||||
})
|
||||
);
|
||||
await deleteFolder({ path, id: createdFolder.id });
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
path: "/",
|
||||
expected: {
|
||||
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
|
||||
length: 3
|
||||
}
|
||||
},
|
||||
{ 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/v2/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
query: {
|
||||
projectId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("folders");
|
||||
expect(payload.folders.length >= expected.folders.length).toBeTruthy();
|
||||
expect(payload).toEqual({
|
||||
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
|
||||
});
|
||||
|
||||
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"
|
||||
})
|
||||
);
|
||||
|
||||
const resUpdatedFolders = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v2/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
query: {
|
||||
projectId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/level1/level2"
|
||||
}
|
||||
});
|
||||
|
||||
expect(resUpdatedFolders.statusCode).toBe(200);
|
||||
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
|
||||
expect(updatedFolderList).toHaveProperty("folders");
|
||||
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/v2/folders/${newFolder.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/level1/level2"
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("folder");
|
||||
expect(payload.folder).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: "folder-updated"
|
||||
})
|
||||
);
|
||||
|
||||
const resUpdatedFolders = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v2/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
query: {
|
||||
projectId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/level1/level2"
|
||||
}
|
||||
});
|
||||
|
||||
expect(resUpdatedFolders.statusCode).toBe(200);
|
||||
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
|
||||
expect(updatedFolderList).toHaveProperty("folders");
|
||||
expect(updatedFolderList.folders.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -70,7 +70,7 @@ const createServiceToken = async (
|
||||
const deleteServiceToken = async () => {
|
||||
const serviceTokenListRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`,
|
||||
url: `/api/v1/projects/${seedData1.project.id}/service-token-data`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
|
||||
@@ -314,8 +314,8 @@ describe("Secret expansion", () => {
|
||||
expect(listSecrets.imports).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretPath: `/__reserve_replication_${secretImportFromProdToDev.id}`,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: "/deep/nested",
|
||||
environment: "prod",
|
||||
secrets: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: "NESTED_KEY_1",
|
||||
|
||||
678
backend/e2e-test/routes/v4/secrets.spec.ts
Normal file
678
backend/e2e-test/routes/v4/secrets.spec.ts
Normal file
@@ -0,0 +1,678 @@
|
||||
import { SecretType } from "@app/db/schemas";
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
type TRawSecret = {
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretComment?: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => {
|
||||
const createSecretReqBody = {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: dto.type || SecretType.Shared,
|
||||
secretPath: dto.path,
|
||||
secretKey: dto.key,
|
||||
secretValue: dto.value,
|
||||
secretComment: dto.comment
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v4/secrets/${dto.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
expect(createSecRes.statusCode).toBe(200);
|
||||
const createdSecretPayload = JSON.parse(createSecRes.payload);
|
||||
expect(createdSecretPayload).toHaveProperty("secret");
|
||||
return createdSecretPayload.secret as TRawSecret;
|
||||
};
|
||||
|
||||
const deleteSecret = async (dto: { path: string; key: string }) => {
|
||||
const deleteSecRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v4/secrets/${dto.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.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 as TRawSecret;
|
||||
};
|
||||
|
||||
describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }])(
|
||||
"Secret V4 - $auth mode",
|
||||
async ({ auth }) => {
|
||||
let folderId = "";
|
||||
let authToken = "";
|
||||
const secretTestCases = [
|
||||
{
|
||||
path: "/",
|
||||
secret: {
|
||||
key: "SEC1",
|
||||
value: "something-secret",
|
||||
comment: "some comment"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/nested1/nested2/folder",
|
||||
secret: {
|
||||
key: "NESTED-SEC1",
|
||||
value: "something-secret",
|
||||
comment: "some comment"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
secret: {
|
||||
key: "secret-key-2",
|
||||
value: `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
|
||||
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
|
||||
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
|
||||
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
|
||||
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
|
||||
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
|
||||
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
|
||||
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
|
||||
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
|
||||
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
|
||||
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
|
||||
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
|
||||
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
|
||||
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
|
||||
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
|
||||
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
|
||||
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
|
||||
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
|
||||
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
|
||||
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
|
||||
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
|
||||
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
|
||||
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
|
||||
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
|
||||
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
|
||||
omQDpP86RX/hIIQ+JyLSaWYa
|
||||
-----END PRIVATE KEY-----`,
|
||||
comment:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/nested1/nested2/folder",
|
||||
secret: {
|
||||
key: "secret-key-3",
|
||||
value: `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
|
||||
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
|
||||
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
|
||||
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
|
||||
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
|
||||
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
|
||||
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
|
||||
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
|
||||
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
|
||||
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
|
||||
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
|
||||
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
|
||||
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
|
||||
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
|
||||
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
|
||||
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
|
||||
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
|
||||
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
|
||||
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
|
||||
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
|
||||
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
|
||||
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
|
||||
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
|
||||
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
|
||||
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
|
||||
omQDpP86RX/hIIQ+JyLSaWYa
|
||||
-----END PRIVATE KEY-----`,
|
||||
comment:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/nested1/nested2/folder",
|
||||
secret: {
|
||||
key: "secret-key-3",
|
||||
value:
|
||||
"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gU2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uCg==",
|
||||
comment: ""
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
if (auth === AuthMode.JWT) {
|
||||
authToken = jwtAuthToken;
|
||||
} else if (auth === AuthMode.IDENTITY_ACCESS_TOKEN) {
|
||||
const identityLogin = await testServer.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/auth/universal-auth/login",
|
||||
body: {
|
||||
clientSecret: seedData1.machineIdentity.clientCredentials.secret,
|
||||
clientId: seedData1.machineIdentity.clientCredentials.id
|
||||
}
|
||||
});
|
||||
expect(identityLogin.statusCode).toBe(200);
|
||||
authToken = identityLogin.json().accessToken;
|
||||
}
|
||||
// create a deep folder
|
||||
const folderCreate = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v2/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
name: "folder",
|
||||
path: "/nested1/nested2"
|
||||
}
|
||||
});
|
||||
expect(folderCreate.statusCode).toBe(200);
|
||||
folderId = folderCreate.json().folder.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const deleteFolder = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v2/folders/${folderId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/nested1/nested2"
|
||||
}
|
||||
});
|
||||
expect(deleteFolder.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
const getSecrets = async (environment: string, secretPath = "/") => {
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v4/secrets`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
query: {
|
||||
secretPath,
|
||||
environment,
|
||||
projectId: seedData1.projectV3.id
|
||||
}
|
||||
});
|
||||
const secrets: TRawSecret[] = JSON.parse(res.payload).secrets || [];
|
||||
return secrets;
|
||||
};
|
||||
|
||||
test.each(secretTestCases)("Create secret in path $path", async ({ secret, path }) => {
|
||||
const createdSecret = await createSecret({ path, ...secret });
|
||||
expect(createdSecret.secretKey).toEqual(secret.key);
|
||||
expect(createdSecret.secretValue).toEqual(secret.value);
|
||||
expect(createdSecret.secretComment || "").toEqual(secret.comment);
|
||||
expect(createdSecret.version).toEqual(1);
|
||||
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Get secret by name in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
|
||||
const getSecByNameRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v4/secrets/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
query: {
|
||||
secretPath: path,
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug
|
||||
}
|
||||
});
|
||||
expect(getSecByNameRes.statusCode).toBe(200);
|
||||
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
|
||||
expect(getSecretByNamePayload).toHaveProperty("secret");
|
||||
const decryptedSecret = getSecretByNamePayload.secret as TRawSecret;
|
||||
expect(decryptedSecret.secretKey).toEqual(secret.key);
|
||||
expect(decryptedSecret.secretValue).toEqual(secret.value);
|
||||
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
|
||||
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
if (auth === AuthMode.JWT) {
|
||||
test.each(secretTestCases)(
|
||||
"Creating personal secret without shared throw error in path $path",
|
||||
async ({ secret }) => {
|
||||
const createSecretReqBody = {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Personal,
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v4/secrets/SEC2`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
const payload = JSON.parse(createSecRes.payload);
|
||||
expect(createSecRes.statusCode).toBe(400);
|
||||
expect(payload.error).toEqual("BadRequest");
|
||||
}
|
||||
);
|
||||
|
||||
test.each(secretTestCases)("Creating personal secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
|
||||
const createSecretReqBody = {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Personal,
|
||||
secretPath: path,
|
||||
secretKey: secret.key,
|
||||
secretValue: "personal-value",
|
||||
secretComment: secret.comment
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v4/secrets/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
expect(createSecRes.statusCode).toBe(200);
|
||||
|
||||
// list secrets should contain personal one and shared one
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
type: SecretType.Shared
|
||||
}),
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: "personal-value",
|
||||
type: SecretType.Personal
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)(
|
||||
"Deleting personal one should not delete shared secret in path $path",
|
||||
async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret }); // shared one
|
||||
await createSecret({ path, ...secret, type: SecretType.Personal });
|
||||
|
||||
// shared secret deletion should delete personal ones also
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Shared
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Personal
|
||||
})
|
||||
])
|
||||
);
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
test.each(secretTestCases)("Update secret in path $path", async ({ path, secret }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
const updateSecretReqBody = {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Shared,
|
||||
secretPath: path,
|
||||
secretKey: secret.key,
|
||||
secretValue: "new-value",
|
||||
secretComment: secret.comment
|
||||
};
|
||||
const updateSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v4/secrets/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: updateSecretReqBody
|
||||
});
|
||||
expect(updateSecRes.statusCode).toBe(200);
|
||||
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
|
||||
expect(updatedSecretPayload).toHaveProperty("secret");
|
||||
const decryptedSecret = updatedSecretPayload.secret;
|
||||
expect(decryptedSecret.secretKey).toEqual(secret.key);
|
||||
expect(decryptedSecret.secretValue).toEqual("new-value");
|
||||
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
|
||||
|
||||
// list secret should have updated value
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: "new-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Delete secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
const deletedSecret = await deleteSecret({ path, key: secret.key });
|
||||
expect(deletedSecret.secretKey).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({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Shared
|
||||
}),
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Personal
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk create secrets in path $path", async ({ secret, path }) => {
|
||||
const createSharedSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v4/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: secret.value,
|
||||
secretComment: 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({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: secret.value,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
|
||||
);
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ ...secret, key: `BULK-${secret.key}-1`, path });
|
||||
|
||||
const createSharedSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v4/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(createSharedSecRes.statusCode).toBe(400);
|
||||
|
||||
await deleteSecret({ path, key: `BULK-${secret.key}-1` });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk update secrets in path $path", async ({ secret, path }) => {
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
|
||||
);
|
||||
|
||||
const updateSharedSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v4/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: 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({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
|
||||
);
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => {
|
||||
const updateSharedSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v4/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
mode: "upsert",
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: 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({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
|
||||
);
|
||||
});
|
||||
|
||||
test("Bulk upsert secrets in path multiple paths", async () => {
|
||||
const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: "comment",
|
||||
secretPath: secretTestCases[0].path
|
||||
}));
|
||||
const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: "comment",
|
||||
secretPath: secretTestCases[1].path
|
||||
}));
|
||||
const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets];
|
||||
|
||||
const updateSharedSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v4/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
mode: "upsert",
|
||||
secrets: testSecrets
|
||||
}
|
||||
});
|
||||
expect(updateSharedSecRes.statusCode).toBe(200);
|
||||
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
|
||||
expect(updateSharedSecPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path);
|
||||
expect(firstBatchSecretsOnInfisical).toEqual(
|
||||
expect.arrayContaining(
|
||||
firstBatchSecrets.map((el) =>
|
||||
expect.objectContaining({
|
||||
secretKey: el.secretKey,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path);
|
||||
expect(secondBatchSecretsOnInfisical).toEqual(
|
||||
expect.arrayContaining(
|
||||
secondBatchSecrets.map((el) =>
|
||||
expect.objectContaining({
|
||||
secretKey: el.secretKey,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey })));
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
|
||||
);
|
||||
|
||||
const deletedSharedSecRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v4/secrets/batch`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
projectId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `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({
|
||||
secretKey: `BULK-${secret.value}-${i + 1}`,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -15,6 +15,7 @@ import { mockSmtpServer } from "./mocks/smtp";
|
||||
import { initDbConnection } from "@app/db";
|
||||
import { queueServiceFactory } from "@app/queue";
|
||||
import { keyStoreFactory } from "@app/keystore/keystore";
|
||||
import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
@@ -62,7 +63,8 @@ export default {
|
||||
|
||||
const smtp = mockSmtpServer();
|
||||
const queue = queueServiceFactory(envCfg, { dbConnectionUrl: envCfg.DB_CONNECTION_URI });
|
||||
const keyStore = keyStoreFactory(envCfg);
|
||||
const keyValueStoreDAL = keyValueStoreDALFactory(db);
|
||||
const keyStore = keyStoreFactory(envCfg, keyValueStoreDAL);
|
||||
|
||||
await queue.initialize();
|
||||
|
||||
|
||||
44
backend/package-lock.json
generated
44
backend/package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"@fastify/multipart": "8.3.1",
|
||||
"@fastify/passport": "^2.4.0",
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/reply-from": "^9.8.0",
|
||||
"@fastify/request-context": "^5.1.0",
|
||||
"@fastify/session": "^10.7.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
@@ -8044,6 +8045,42 @@
|
||||
"toad-cache": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/reply-from": {
|
||||
"version": "9.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.8.0.tgz",
|
||||
"integrity": "sha512-bPNVaFhEeNI0Lyl6404YZaPFokudCplidE3QoOcr78yOy6H9sYw97p5KPYvY/NJNUHfFtvxOaSAHnK+YSiv/Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/error": "^3.0.0",
|
||||
"end-of-stream": "^1.4.4",
|
||||
"fast-content-type-parse": "^1.1.0",
|
||||
"fast-querystring": "^1.0.0",
|
||||
"fastify-plugin": "^4.0.0",
|
||||
"toad-cache": "^3.7.0",
|
||||
"undici": "^5.19.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/reply-from/node_modules/@fastify/busboy": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
|
||||
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/reply-from/node_modules/undici": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
|
||||
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/request-context": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz",
|
||||
@@ -29330,9 +29367,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/toad-cache": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.3.0.tgz",
|
||||
"integrity": "sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==",
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
|
||||
"integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"@fastify/multipart": "8.3.1",
|
||||
"@fastify/passport": "^2.4.0",
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/reply-from": "^9.8.0",
|
||||
"@fastify/request-context": "^5.1.0",
|
||||
"@fastify/session": "^10.7.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
|
||||
15
backend/src/@types/fastify.d.ts
vendored
15
backend/src/@types/fastify.d.ts
vendored
@@ -1,13 +1,13 @@
|
||||
import "fastify";
|
||||
|
||||
import { Redis } from "ioredis";
|
||||
import { Cluster, Redis } from "ioredis";
|
||||
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types";
|
||||
import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
|
||||
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
|
||||
@@ -16,6 +16,7 @@ import { TEventBusService } from "@app/ee/services/event/event-bus-service";
|
||||
import { TServerSentEventsService } from "@app/ee/services/event/event-sse-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template";
|
||||
@@ -32,6 +33,7 @@ import { TPitServiceFactory } from "@app/ee/services/pit/pit-service";
|
||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types";
|
||||
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
import { RateLimitConfiguration, TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-types";
|
||||
import { TRelayServiceFactory } from "@app/ee/services/relay/relay-service";
|
||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types";
|
||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-types";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
@@ -83,6 +85,8 @@ import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua
|
||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { TOfflineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service";
|
||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
@@ -161,6 +165,7 @@ declare module "fastify" {
|
||||
};
|
||||
// identity injection. depending on which kinda of token the information is filled in auth
|
||||
auth: TAuthMode;
|
||||
shouldForwardWritesToPrimaryInstance: boolean;
|
||||
permission: {
|
||||
authMethod: ActorAuthMethod;
|
||||
type: ActorType;
|
||||
@@ -194,7 +199,7 @@ declare module "fastify" {
|
||||
}
|
||||
|
||||
interface FastifyInstance {
|
||||
redis: Redis;
|
||||
redis: Redis | Cluster;
|
||||
services: {
|
||||
login: TAuthLoginFactory;
|
||||
password: TAuthPasswordFactory;
|
||||
@@ -293,6 +298,8 @@ declare module "fastify" {
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
relay: TRelayServiceFactory;
|
||||
gatewayV2: TGatewayV2ServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
folderCommit: TFolderCommitServiceFactory;
|
||||
pit: TPitServiceFactory;
|
||||
@@ -303,6 +310,8 @@ declare module "fastify" {
|
||||
bus: TEventBusService;
|
||||
sse: TServerSentEventsService;
|
||||
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
|
||||
notification: TNotificationServiceFactory;
|
||||
offlineUsageReport: TOfflineUsageReportServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
50
backend/src/@types/knex.d.ts
vendored
50
backend/src/@types/knex.d.ts
vendored
@@ -101,6 +101,9 @@ import {
|
||||
TGateways,
|
||||
TGatewaysInsert,
|
||||
TGatewaysUpdate,
|
||||
TGatewaysV2,
|
||||
TGatewaysV2Insert,
|
||||
TGatewaysV2Update,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
@@ -179,6 +182,9 @@ import {
|
||||
TIncidentContacts,
|
||||
TIncidentContactsInsert,
|
||||
TIncidentContactsUpdate,
|
||||
TInstanceRelayConfig,
|
||||
TInstanceRelayConfigInsert,
|
||||
TInstanceRelayConfigUpdate,
|
||||
TIntegrationAuths,
|
||||
TIntegrationAuthsInsert,
|
||||
TIntegrationAuthsUpdate,
|
||||
@@ -191,6 +197,9 @@ import {
|
||||
TInternalKms,
|
||||
TInternalKmsInsert,
|
||||
TInternalKmsUpdate,
|
||||
TKeyValueStore,
|
||||
TKeyValueStoreInsert,
|
||||
TKeyValueStoreUpdate,
|
||||
TKmipClientCertificates,
|
||||
TKmipClientCertificatesInsert,
|
||||
TKmipClientCertificatesUpdate,
|
||||
@@ -230,9 +239,15 @@ import {
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate,
|
||||
TOrgGatewayConfigV2,
|
||||
TOrgGatewayConfigV2Insert,
|
||||
TOrgGatewayConfigV2Update,
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate,
|
||||
TOrgRelayConfig,
|
||||
TOrgRelayConfigInsert,
|
||||
TOrgRelayConfigUpdate,
|
||||
TOrgRoles,
|
||||
TOrgRolesInsert,
|
||||
TOrgRolesUpdate,
|
||||
@@ -290,6 +305,9 @@ import {
|
||||
TRateLimit,
|
||||
TRateLimitInsert,
|
||||
TRateLimitUpdate,
|
||||
TRelays,
|
||||
TRelaysInsert,
|
||||
TRelaysUpdate,
|
||||
TResourceMetadata,
|
||||
TResourceMetadataInsert,
|
||||
TResourceMetadataUpdate,
|
||||
@@ -530,6 +548,11 @@ import {
|
||||
TSecretReminderRecipientsInsert,
|
||||
TSecretReminderRecipientsUpdate
|
||||
} from "@app/db/schemas/secret-reminder-recipients";
|
||||
import {
|
||||
TUserNotifications,
|
||||
TUserNotificationsInsert,
|
||||
TUserNotificationsUpdate
|
||||
} from "@app/db/schemas/user-notifications";
|
||||
|
||||
declare module "knex" {
|
||||
namespace Knex {
|
||||
@@ -1233,6 +1256,17 @@ declare module "knex/types/tables" {
|
||||
TSecretScanningResourcesInsert,
|
||||
TSecretScanningResourcesUpdate
|
||||
>;
|
||||
[TableName.InstanceRelayConfig]: KnexOriginal.CompositeTableType<
|
||||
TInstanceRelayConfig,
|
||||
TInstanceRelayConfigInsert,
|
||||
TInstanceRelayConfigUpdate
|
||||
>;
|
||||
[TableName.OrgRelayConfig]: KnexOriginal.CompositeTableType<
|
||||
TOrgRelayConfig,
|
||||
TOrgRelayConfigInsert,
|
||||
TOrgRelayConfigUpdate
|
||||
>;
|
||||
[TableName.Relay]: KnexOriginal.CompositeTableType<TRelays, TRelaysInsert, TRelaysUpdate>;
|
||||
[TableName.SecretScanningScan]: KnexOriginal.CompositeTableType<
|
||||
TSecretScanningScans,
|
||||
TSecretScanningScansInsert,
|
||||
@@ -1254,5 +1288,21 @@ declare module "knex/types/tables" {
|
||||
TRemindersRecipientsInsert,
|
||||
TRemindersRecipientsUpdate
|
||||
>;
|
||||
[TableName.OrgGatewayConfigV2]: KnexOriginal.CompositeTableType<
|
||||
TOrgGatewayConfigV2,
|
||||
TOrgGatewayConfigV2Insert,
|
||||
TOrgGatewayConfigV2Update
|
||||
>;
|
||||
[TableName.GatewayV2]: KnexOriginal.CompositeTableType<TGatewaysV2, TGatewaysV2Insert, TGatewaysV2Update>;
|
||||
[TableName.UserNotifications]: KnexOriginal.CompositeTableType<
|
||||
TUserNotifications,
|
||||
TUserNotificationsInsert,
|
||||
TUserNotificationsUpdate
|
||||
>;
|
||||
[TableName.KeyValueStore]: KnexOriginal.CompositeTableType<
|
||||
TKeyValueStore,
|
||||
TKeyValueStoreInsert,
|
||||
TKeyValueStoreUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
@@ -13,9 +14,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.dropUnique(["orgId", "name"]);
|
||||
});
|
||||
await dropConstraintIfExists(TableName.AppConnection, "app_connections_orgid_name_unique", knex);
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretSync, (t) => {
|
||||
t.dropUnique(["projectId", "name"]);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.IdentityLdapAuth)) {
|
||||
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutEnabled");
|
||||
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutThreshold");
|
||||
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDurationSeconds");
|
||||
const hasLockoutCounterReset = await knex.schema.hasColumn(
|
||||
TableName.IdentityLdapAuth,
|
||||
"lockoutCounterResetSeconds"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
if (!hasLockoutEnabled) {
|
||||
t.boolean("lockoutEnabled").notNullable().defaultTo(true);
|
||||
}
|
||||
if (!hasLockoutThreshold) {
|
||||
t.integer("lockoutThreshold").notNullable().defaultTo(3);
|
||||
}
|
||||
if (!hasLockoutDuration) {
|
||||
t.integer("lockoutDurationSeconds").notNullable().defaultTo(300); // 5 minutes
|
||||
}
|
||||
if (!hasLockoutCounterReset) {
|
||||
t.integer("lockoutCounterResetSeconds").notNullable().defaultTo(30); // 30 seconds
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.IdentityLdapAuth)) {
|
||||
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutEnabled");
|
||||
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutThreshold");
|
||||
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityLdapAuth, "lockoutDurationSeconds");
|
||||
const hasLockoutCounterReset = await knex.schema.hasColumn(
|
||||
TableName.IdentityLdapAuth,
|
||||
"lockoutCounterResetSeconds"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
if (hasLockoutEnabled) {
|
||||
t.dropColumn("lockoutEnabled");
|
||||
}
|
||||
if (hasLockoutThreshold) {
|
||||
t.dropColumn("lockoutThreshold");
|
||||
}
|
||||
if (hasLockoutDuration) {
|
||||
t.dropColumn("lockoutDurationSeconds");
|
||||
}
|
||||
if (hasLockoutCounterReset) {
|
||||
t.dropColumn("lockoutCounterResetSeconds");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.InstanceRelayConfig))) {
|
||||
await knex.schema.createTable(TableName.InstanceRelayConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
// Root CA for relay PKI
|
||||
t.binary("encryptedRootRelayPkiCaPrivateKey").notNullable();
|
||||
t.binary("encryptedRootRelayPkiCaCertificate").notNullable();
|
||||
|
||||
// Instance CA for relay PKI
|
||||
t.binary("encryptedInstanceRelayPkiCaPrivateKey").notNullable();
|
||||
t.binary("encryptedInstanceRelayPkiCaCertificate").notNullable();
|
||||
t.binary("encryptedInstanceRelayPkiCaCertificateChain").notNullable();
|
||||
|
||||
// Instance client/server intermediates for relay PKI
|
||||
t.binary("encryptedInstanceRelayPkiClientCaPrivateKey").notNullable();
|
||||
t.binary("encryptedInstanceRelayPkiClientCaCertificate").notNullable();
|
||||
t.binary("encryptedInstanceRelayPkiClientCaCertificateChain").notNullable();
|
||||
t.binary("encryptedInstanceRelayPkiServerCaPrivateKey").notNullable();
|
||||
t.binary("encryptedInstanceRelayPkiServerCaCertificate").notNullable();
|
||||
t.binary("encryptedInstanceRelayPkiServerCaCertificateChain").notNullable();
|
||||
|
||||
// Org Parent CAs for relay
|
||||
t.binary("encryptedOrgRelayPkiCaPrivateKey").notNullable();
|
||||
t.binary("encryptedOrgRelayPkiCaCertificate").notNullable();
|
||||
t.binary("encryptedOrgRelayPkiCaCertificateChain").notNullable();
|
||||
|
||||
// Instance SSH CAs for relay
|
||||
t.binary("encryptedInstanceRelaySshClientCaPrivateKey").notNullable();
|
||||
t.binary("encryptedInstanceRelaySshClientCaPublicKey").notNullable();
|
||||
t.binary("encryptedInstanceRelaySshServerCaPrivateKey").notNullable();
|
||||
t.binary("encryptedInstanceRelaySshServerCaPublicKey").notNullable();
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.InstanceRelayConfig);
|
||||
}
|
||||
|
||||
// Org-level relay configuration (one-to-one with organization)
|
||||
if (!(await knex.schema.hasTable(TableName.OrgRelayConfig))) {
|
||||
await knex.schema.createTable(TableName.OrgRelayConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.uuid("orgId").notNullable().unique();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
// Org-scoped relay PKI (client + server)
|
||||
t.binary("encryptedRelayPkiClientCaPrivateKey").notNullable();
|
||||
t.binary("encryptedRelayPkiClientCaCertificate").notNullable();
|
||||
t.binary("encryptedRelayPkiClientCaCertificateChain").notNullable();
|
||||
t.binary("encryptedRelayPkiServerCaPrivateKey").notNullable();
|
||||
t.binary("encryptedRelayPkiServerCaCertificate").notNullable();
|
||||
t.binary("encryptedRelayPkiServerCaCertificateChain").notNullable();
|
||||
|
||||
// Org-scoped relay SSH (client + server)
|
||||
t.binary("encryptedRelaySshClientCaPrivateKey").notNullable();
|
||||
t.binary("encryptedRelaySshClientCaPublicKey").notNullable();
|
||||
t.binary("encryptedRelaySshServerCaPrivateKey").notNullable();
|
||||
t.binary("encryptedRelaySshServerCaPublicKey").notNullable();
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.OrgRelayConfig);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.OrgGatewayConfigV2))) {
|
||||
await knex.schema.createTable(TableName.OrgGatewayConfigV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("orgId").notNullable().unique();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
t.binary("encryptedRootGatewayCaPrivateKey").notNullable();
|
||||
t.binary("encryptedRootGatewayCaCertificate").notNullable();
|
||||
t.binary("encryptedGatewayServerCaPrivateKey").notNullable();
|
||||
t.binary("encryptedGatewayServerCaCertificate").notNullable();
|
||||
t.binary("encryptedGatewayServerCaCertificateChain").notNullable();
|
||||
t.binary("encryptedGatewayClientCaPrivateKey").notNullable();
|
||||
t.binary("encryptedGatewayClientCaCertificate").notNullable();
|
||||
t.binary("encryptedGatewayClientCaCertificateChain").notNullable();
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.OrgGatewayConfigV2);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.Relay))) {
|
||||
await knex.schema.createTable(TableName.Relay, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.uuid("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
t.uuid("identityId");
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.string("host").notNullable();
|
||||
|
||||
t.unique(["orgId", "name"]);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.Relay);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.GatewayV2))) {
|
||||
await knex.schema.createTable(TableName.GatewayV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
t.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
|
||||
t.uuid("relayId");
|
||||
t.foreign("relayId").references("id").inTable(TableName.Relay).onDelete("SET NULL");
|
||||
|
||||
t.string("name").notNullable();
|
||||
|
||||
t.unique(["orgId", "name"]);
|
||||
|
||||
t.dateTime("heartbeat");
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.GatewayV2);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await dropOnUpdateTrigger(knex, TableName.OrgRelayConfig);
|
||||
await knex.schema.dropTableIfExists(TableName.OrgRelayConfig);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.InstanceRelayConfig);
|
||||
await knex.schema.dropTableIfExists(TableName.InstanceRelayConfig);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfigV2);
|
||||
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfigV2);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.GatewayV2);
|
||||
await knex.schema.dropTableIfExists(TableName.GatewayV2);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.Relay);
|
||||
await knex.schema.dropTableIfExists(TableName.Relay);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.UserNotifications))) {
|
||||
const createTableSql = knex.schema
|
||||
.createTable(TableName.UserNotifications, (t) => {
|
||||
t.uuid("id").defaultTo(knex.fn.uuid());
|
||||
t.uuid("userId").notNullable();
|
||||
t.uuid("orgId").nullable();
|
||||
|
||||
t.string("type").notNullable();
|
||||
t.string("title").notNullable(); // Markdown
|
||||
t.text("body").nullable(); // Markdown
|
||||
t.string("link").nullable();
|
||||
t.boolean("isRead").notNullable().defaultTo(false);
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.primary(["id", "createdAt"]);
|
||||
})
|
||||
.toString();
|
||||
|
||||
await knex.schema.raw(`
|
||||
${createTableSql} PARTITION BY RANGE ("createdAt");
|
||||
`);
|
||||
|
||||
await knex.schema.raw(
|
||||
`CREATE TABLE ${TableName.UserNotifications}_default PARTITION OF ${TableName.UserNotifications} DEFAULT`
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.UserNotifications, (t) => {
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
t.index("type");
|
||||
t.index(["userId", "isRead"]);
|
||||
t.index(["userId", "createdAt", "orgId"]);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.UserNotifications);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.UserNotifications);
|
||||
await dropOnUpdateTrigger(knex, TableName.UserNotifications);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayV2Id"))) {
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
|
||||
table.uuid("gatewayV2Id");
|
||||
table.foreign("gatewayV2Id").references("id").inTable(TableName.GatewayV2).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayV2Id"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.uuid("gatewayV2Id");
|
||||
table.foreign("gatewayV2Id").references("id").inTable(TableName.GatewayV2).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayV2Id")) {
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
|
||||
table.dropColumn("gatewayV2Id");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayV2Id")) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.dropColumn("gatewayV2Id");
|
||||
});
|
||||
}
|
||||
}
|
||||
221
backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts
Normal file
221
backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
|
||||
import { SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
||||
import { createCircularCache } from "./utils/ring-buffer";
|
||||
import { getMigrationEncryptionServices } from "./utils/services";
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.AuditLogStream)) {
|
||||
const hasProvider = await knex.schema.hasColumn(TableName.AuditLogStream, "provider");
|
||||
const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials");
|
||||
|
||||
await knex.schema.alterTable(TableName.AuditLogStream, (t) => {
|
||||
if (!hasProvider) t.string("provider").notNullable().defaultTo("custom");
|
||||
if (!hasEncryptedCredentials) t.binary("encryptedCredentials");
|
||||
|
||||
// This column will no longer be used but we're not dropping it so that we can have a backup in case the migration goes wrong
|
||||
t.string("url").nullable().alter();
|
||||
});
|
||||
|
||||
if (!hasEncryptedCredentials) {
|
||||
const superAdminDAL = superAdminDALFactory(knex);
|
||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
||||
const keyStore = inMemoryKeyStore();
|
||||
|
||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||
|
||||
const orgEncryptionRingBuffer =
|
||||
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
||||
|
||||
const logStreams = await knex(TableName.AuditLogStream).select(
|
||||
"id",
|
||||
"orgId",
|
||||
|
||||
"url",
|
||||
"encryptedHeadersAlgorithm",
|
||||
"encryptedHeadersCiphertext",
|
||||
"encryptedHeadersIV",
|
||||
"encryptedHeadersKeyEncoding",
|
||||
"encryptedHeadersTag"
|
||||
);
|
||||
|
||||
const updatedLogStreams = await Promise.all(
|
||||
logStreams.map(async (el) => {
|
||||
let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId);
|
||||
if (!orgKmsService) {
|
||||
orgKmsService = await kmsService.createCipherPairWithDataKey(
|
||||
{
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: el.orgId
|
||||
},
|
||||
knex
|
||||
);
|
||||
orgEncryptionRingBuffer.push(el.orgId, orgKmsService);
|
||||
}
|
||||
|
||||
const provider = "custom";
|
||||
let credentials;
|
||||
|
||||
if (
|
||||
el.encryptedHeadersTag &&
|
||||
el.encryptedHeadersIV &&
|
||||
el.encryptedHeadersCiphertext &&
|
||||
el.encryptedHeadersKeyEncoding
|
||||
) {
|
||||
const decryptedHeaders = crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.decryptWithRootEncryptionKey({
|
||||
tag: el.encryptedHeadersTag,
|
||||
iv: el.encryptedHeadersIV,
|
||||
ciphertext: el.encryptedHeadersCiphertext,
|
||||
keyEncoding: el.encryptedHeadersKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
|
||||
credentials = {
|
||||
url: el.url,
|
||||
headers: JSON.parse(decryptedHeaders)
|
||||
};
|
||||
} else {
|
||||
credentials = {
|
||||
url: el.url,
|
||||
headers: []
|
||||
};
|
||||
}
|
||||
|
||||
const encryptedCredentials = orgKmsService.encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(credentials), "utf8")
|
||||
}).cipherTextBlob;
|
||||
|
||||
return {
|
||||
id: el.id,
|
||||
orgId: el.orgId,
|
||||
url: el.url,
|
||||
provider,
|
||||
encryptedCredentials
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (let i = 0; i < updatedLogStreams.length; i += BATCH_SIZE) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.AuditLogStream)
|
||||
.insert(updatedLogStreams.slice(i, i + BATCH_SIZE))
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.AuditLogStream, (t) => {
|
||||
t.binary("encryptedCredentials").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: The down migration does not utilize the existing "url" and encrypted header columns
|
||||
// because we're taking the latest data from the credentials column and re-encrypting it into relevant columns
|
||||
//
|
||||
// If this down migration was to fail, you can fall-back to the existing URL and encrypted header columns to retrieve
|
||||
// data that was created prior to this migration
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.AuditLogStream)) {
|
||||
const hasProvider = await knex.schema.hasColumn(TableName.AuditLogStream, "provider");
|
||||
const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials");
|
||||
|
||||
if (hasEncryptedCredentials) {
|
||||
const superAdminDAL = superAdminDALFactory(knex);
|
||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
||||
const keyStore = inMemoryKeyStore();
|
||||
|
||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||
|
||||
const orgEncryptionRingBuffer =
|
||||
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
||||
|
||||
const logStreamsToRevert = await knex(TableName.AuditLogStream)
|
||||
.select("id", "orgId", "encryptedCredentials")
|
||||
.where("provider", "custom")
|
||||
.whereNotNull("encryptedCredentials");
|
||||
|
||||
const updatedLogStreams = await Promise.all(
|
||||
logStreamsToRevert.map(async (el) => {
|
||||
let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId);
|
||||
if (!orgKmsService) {
|
||||
orgKmsService = await kmsService.createCipherPairWithDataKey(
|
||||
{
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: el.orgId
|
||||
},
|
||||
knex
|
||||
);
|
||||
orgEncryptionRingBuffer.push(el.orgId, orgKmsService);
|
||||
}
|
||||
|
||||
const decryptedCredentials = orgKmsService
|
||||
.decryptor({
|
||||
cipherTextBlob: el.encryptedCredentials
|
||||
})
|
||||
.toString();
|
||||
|
||||
const credentials: { url: string; headers: { key: string; value: string }[] } =
|
||||
JSON.parse(decryptedCredentials);
|
||||
|
||||
const originalUrl: string = credentials.url;
|
||||
|
||||
const encryptedHeadersResult = crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.encryptWithRootEncryptionKey(JSON.stringify(credentials.headers), envConfig);
|
||||
|
||||
const encryptedHeadersAlgorithm: string = encryptedHeadersResult.algorithm;
|
||||
const encryptedHeadersCiphertext: string = encryptedHeadersResult.ciphertext;
|
||||
const encryptedHeadersIV: string = encryptedHeadersResult.iv;
|
||||
const encryptedHeadersKeyEncoding: string = encryptedHeadersResult.encoding;
|
||||
const encryptedHeadersTag: string = encryptedHeadersResult.tag;
|
||||
|
||||
return {
|
||||
id: el.id,
|
||||
orgId: el.orgId,
|
||||
encryptedCredentials: el.encryptedCredentials,
|
||||
|
||||
url: originalUrl,
|
||||
encryptedHeadersAlgorithm,
|
||||
encryptedHeadersCiphertext,
|
||||
encryptedHeadersIV,
|
||||
encryptedHeadersKeyEncoding,
|
||||
encryptedHeadersTag
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (let i = 0; i < updatedLogStreams.length; i += BATCH_SIZE) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.AuditLogStream)
|
||||
.insert(updatedLogStreams.slice(i, i + BATCH_SIZE))
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
}
|
||||
|
||||
await knex(TableName.AuditLogStream)
|
||||
.where((qb) => {
|
||||
void qb.whereNot("provider", "custom").orWhereNull("url");
|
||||
})
|
||||
.del();
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.AuditLogStream, (t) => {
|
||||
t.string("url").notNullable().alter();
|
||||
|
||||
if (hasProvider) t.dropColumn("provider");
|
||||
if (hasEncryptedCredentials) t.dropColumn("encryptedCredentials");
|
||||
});
|
||||
}
|
||||
}
|
||||
18
backend/src/db/migrations/20250908193226_sql-cache_int.ts
Normal file
18
backend/src/db/migrations/20250908193226_sql-cache_int.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.KeyValueStore))) {
|
||||
await knex.schema.createTable(TableName.KeyValueStore, (t) => {
|
||||
t.text("key").primary();
|
||||
t.bigint("integerValue");
|
||||
t.datetime("expiresAt");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.KeyValueStore);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasPayloadCol = await knex.schema.hasColumn(TableName.AuthTokens, "payload");
|
||||
|
||||
if (!hasPayloadCol) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.text("payload").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasPayloadCol = await knex.schema.hasColumn(TableName.AuthTokens, "payload");
|
||||
|
||||
if (hasPayloadCol) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.dropColumn("payload");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
const UNIQUE_NAME_ORG_CONNECTION_INDEX = "unique_name_org_app_connection";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.AppConnection)) {
|
||||
// we can't add the constraint back after up since there may be conflicting names so we do if exists
|
||||
await dropConstraintIfExists(TableName.AppConnection, "app_connections_orgid_name_unique", knex);
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.AppConnection, "projectId"))) {
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.string("projectId").nullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
// unique name for project-level connections
|
||||
t.unique(["name", "projectId", "orgId"]);
|
||||
});
|
||||
|
||||
// unique name for org-level connections
|
||||
await knex.raw(`
|
||||
CREATE UNIQUE INDEX ${UNIQUE_NAME_ORG_CONNECTION_INDEX}
|
||||
ON ${TableName.AppConnection} ("name", "orgId")
|
||||
WHERE "projectId" IS NULL
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.AppConnection)) {
|
||||
if (await knex.schema.hasColumn(TableName.AppConnection, "projectId")) {
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.dropUnique(["name", "projectId", "orgId"]);
|
||||
t.dropColumn("projectId");
|
||||
});
|
||||
await dropConstraintIfExists(TableName.AppConnection, UNIQUE_NAME_ORG_CONNECTION_INDEX, knex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ export const AppConnectionsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional()
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
projectId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AuditLogStreamsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
url: z.string(),
|
||||
url: z.string().nullable().optional(),
|
||||
encryptedHeadersCiphertext: z.string().nullable().optional(),
|
||||
encryptedHeadersIV: z.string().nullable().optional(),
|
||||
encryptedHeadersTag: z.string().nullable().optional(),
|
||||
@@ -17,7 +19,9 @@ export const AuditLogStreamsSchema = z.object({
|
||||
encryptedHeadersKeyEncoding: z.string().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
provider: z.string().default("custom"),
|
||||
encryptedCredentials: zodBuffer
|
||||
});
|
||||
|
||||
export type TAuditLogStreams = z.infer<typeof AuditLogStreamsSchema>;
|
||||
|
||||
@@ -18,7 +18,8 @@ export const AuthTokensSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
aliasId: z.string().nullable().optional()
|
||||
aliasId: z.string().nullable().optional(),
|
||||
payload: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
|
||||
@@ -29,7 +29,8 @@ export const DynamicSecretsSchema = z.object({
|
||||
encryptedInput: zodBuffer,
|
||||
projectGatewayId: z.string().uuid().nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
usernameTemplate: z.string().nullable().optional(),
|
||||
gatewayV2Id: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
|
||||
23
backend/src/db/schemas/gateways-v2.ts
Normal file
23
backend/src/db/schemas/gateways-v2.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const GatewaysV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
identityId: z.string().uuid(),
|
||||
relayId: z.string().uuid().nullable().optional(),
|
||||
name: z.string(),
|
||||
heartbeat: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TGatewaysV2 = z.infer<typeof GatewaysV2Schema>;
|
||||
export type TGatewaysV2Insert = Omit<z.input<typeof GatewaysV2Schema>, TImmutableDBKeys>;
|
||||
export type TGatewaysV2Update = Partial<Omit<z.input<typeof GatewaysV2Schema>, TImmutableDBKeys>>;
|
||||
@@ -32,7 +32,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0),
|
||||
tokenReviewMode: z.string().default("api")
|
||||
tokenReviewMode: z.string().default("api"),
|
||||
gatewayV2Id: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
|
||||
|
||||
@@ -26,7 +26,11 @@ export const IdentityLdapAuthsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
accessTokenPeriod: z.coerce.number().default(0),
|
||||
templateId: z.string().uuid().nullable().optional()
|
||||
templateId: z.string().uuid().nullable().optional(),
|
||||
lockoutEnabled: z.boolean().default(true),
|
||||
lockoutThreshold: z.number().default(3),
|
||||
lockoutDurationSeconds: z.number().default(300),
|
||||
lockoutCounterResetSeconds: z.number().default(30)
|
||||
});
|
||||
|
||||
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
||||
|
||||
@@ -31,6 +31,7 @@ export * from "./folder-commits";
|
||||
export * from "./folder-tree-checkpoint-resources";
|
||||
export * from "./folder-tree-checkpoints";
|
||||
export * from "./gateways";
|
||||
export * from "./gateways-v2";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./github-org-sync-configs";
|
||||
@@ -57,10 +58,12 @@ export * from "./identity-token-auths";
|
||||
export * from "./identity-ua-client-secrets";
|
||||
export * from "./identity-universal-auths";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./instance-relay-config";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./internal-certificate-authorities";
|
||||
export * from "./internal-kms";
|
||||
export * from "./key-value-store";
|
||||
export * from "./kmip-client-certificates";
|
||||
export * from "./kmip-clients";
|
||||
export * from "./kmip-org-configs";
|
||||
@@ -75,7 +78,9 @@ export * from "./models";
|
||||
export * from "./oidc-configs";
|
||||
export * from "./org-bots";
|
||||
export * from "./org-gateway-config";
|
||||
export * from "./org-gateway-config-v2";
|
||||
export * from "./org-memberships";
|
||||
export * from "./org-relay-config";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
export * from "./pki-alerts";
|
||||
@@ -96,6 +101,7 @@ export * from "./project-user-additional-privilege";
|
||||
export * from "./project-user-membership-roles";
|
||||
export * from "./projects";
|
||||
export * from "./rate-limit";
|
||||
export * from "./relays";
|
||||
export * from "./resource-metadata";
|
||||
export * from "./saml-configs";
|
||||
export * from "./scim-tokens";
|
||||
|
||||
38
backend/src/db/schemas/instance-relay-config.ts
Normal file
38
backend/src/db/schemas/instance-relay-config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const InstanceRelayConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedRootRelayPkiCaPrivateKey: zodBuffer,
|
||||
encryptedRootRelayPkiCaCertificate: zodBuffer,
|
||||
encryptedInstanceRelayPkiCaPrivateKey: zodBuffer,
|
||||
encryptedInstanceRelayPkiCaCertificate: zodBuffer,
|
||||
encryptedInstanceRelayPkiCaCertificateChain: zodBuffer,
|
||||
encryptedInstanceRelayPkiClientCaPrivateKey: zodBuffer,
|
||||
encryptedInstanceRelayPkiClientCaCertificate: zodBuffer,
|
||||
encryptedInstanceRelayPkiClientCaCertificateChain: zodBuffer,
|
||||
encryptedInstanceRelayPkiServerCaPrivateKey: zodBuffer,
|
||||
encryptedInstanceRelayPkiServerCaCertificate: zodBuffer,
|
||||
encryptedInstanceRelayPkiServerCaCertificateChain: zodBuffer,
|
||||
encryptedOrgRelayPkiCaPrivateKey: zodBuffer,
|
||||
encryptedOrgRelayPkiCaCertificate: zodBuffer,
|
||||
encryptedOrgRelayPkiCaCertificateChain: zodBuffer,
|
||||
encryptedInstanceRelaySshClientCaPrivateKey: zodBuffer,
|
||||
encryptedInstanceRelaySshClientCaPublicKey: zodBuffer,
|
||||
encryptedInstanceRelaySshServerCaPrivateKey: zodBuffer,
|
||||
encryptedInstanceRelaySshServerCaPublicKey: zodBuffer
|
||||
});
|
||||
|
||||
export type TInstanceRelayConfig = z.infer<typeof InstanceRelayConfigSchema>;
|
||||
export type TInstanceRelayConfigInsert = Omit<z.input<typeof InstanceRelayConfigSchema>, TImmutableDBKeys>;
|
||||
export type TInstanceRelayConfigUpdate = Partial<Omit<z.input<typeof InstanceRelayConfigSchema>, TImmutableDBKeys>>;
|
||||
20
backend/src/db/schemas/key-value-store.ts
Normal file
20
backend/src/db/schemas/key-value-store.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const KeyValueStoreSchema = z.object({
|
||||
key: z.string(),
|
||||
integerValue: z.coerce.number().nullable().optional(),
|
||||
expiresAt: z.date().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TKeyValueStore = z.infer<typeof KeyValueStoreSchema>;
|
||||
export type TKeyValueStoreInsert = Omit<z.input<typeof KeyValueStoreSchema>, TImmutableDBKeys>;
|
||||
export type TKeyValueStoreUpdate = Partial<Omit<z.input<typeof KeyValueStoreSchema>, TImmutableDBKeys>>;
|
||||
@@ -131,6 +131,7 @@ export enum TableName {
|
||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||
UserNotifications = "user_notifications",
|
||||
// Gateway
|
||||
OrgGatewayConfig = "org_gateway_config",
|
||||
Gateway = "gateways",
|
||||
@@ -178,7 +179,16 @@ export enum TableName {
|
||||
SecretScanningConfig = "secret_scanning_configs",
|
||||
// reminders
|
||||
Reminder = "reminders",
|
||||
ReminderRecipient = "reminders_recipients"
|
||||
ReminderRecipient = "reminders_recipients",
|
||||
|
||||
// gateway v2
|
||||
InstanceRelayConfig = "instance_relay_config",
|
||||
OrgRelayConfig = "org_relay_config",
|
||||
OrgGatewayConfigV2 = "org_gateway_config_v2",
|
||||
Relay = "relays",
|
||||
GatewayV2 = "gateways_v2",
|
||||
|
||||
KeyValueStore = "key_value_store"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
|
||||
|
||||
29
backend/src/db/schemas/org-gateway-config-v2.ts
Normal file
29
backend/src/db/schemas/org-gateway-config-v2.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrgGatewayConfigV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedRootGatewayCaPrivateKey: zodBuffer,
|
||||
encryptedRootGatewayCaCertificate: zodBuffer,
|
||||
encryptedGatewayServerCaPrivateKey: zodBuffer,
|
||||
encryptedGatewayServerCaCertificate: zodBuffer,
|
||||
encryptedGatewayServerCaCertificateChain: zodBuffer,
|
||||
encryptedGatewayClientCaPrivateKey: zodBuffer,
|
||||
encryptedGatewayClientCaCertificate: zodBuffer,
|
||||
encryptedGatewayClientCaCertificateChain: zodBuffer
|
||||
});
|
||||
|
||||
export type TOrgGatewayConfigV2 = z.infer<typeof OrgGatewayConfigV2Schema>;
|
||||
export type TOrgGatewayConfigV2Insert = Omit<z.input<typeof OrgGatewayConfigV2Schema>, TImmutableDBKeys>;
|
||||
export type TOrgGatewayConfigV2Update = Partial<Omit<z.input<typeof OrgGatewayConfigV2Schema>, TImmutableDBKeys>>;
|
||||
31
backend/src/db/schemas/org-relay-config.ts
Normal file
31
backend/src/db/schemas/org-relay-config.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrgRelayConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
encryptedRelayPkiClientCaPrivateKey: zodBuffer,
|
||||
encryptedRelayPkiClientCaCertificate: zodBuffer,
|
||||
encryptedRelayPkiClientCaCertificateChain: zodBuffer,
|
||||
encryptedRelayPkiServerCaPrivateKey: zodBuffer,
|
||||
encryptedRelayPkiServerCaCertificate: zodBuffer,
|
||||
encryptedRelayPkiServerCaCertificateChain: zodBuffer,
|
||||
encryptedRelaySshClientCaPrivateKey: zodBuffer,
|
||||
encryptedRelaySshClientCaPublicKey: zodBuffer,
|
||||
encryptedRelaySshServerCaPrivateKey: zodBuffer,
|
||||
encryptedRelaySshServerCaPublicKey: zodBuffer
|
||||
});
|
||||
|
||||
export type TOrgRelayConfig = z.infer<typeof OrgRelayConfigSchema>;
|
||||
export type TOrgRelayConfigInsert = Omit<z.input<typeof OrgRelayConfigSchema>, TImmutableDBKeys>;
|
||||
export type TOrgRelayConfigUpdate = Partial<Omit<z.input<typeof OrgRelayConfigSchema>, TImmutableDBKeys>>;
|
||||
22
backend/src/db/schemas/relays.ts
Normal file
22
backend/src/db/schemas/relays.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const RelaysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
identityId: z.string().uuid().nullable().optional(),
|
||||
name: z.string(),
|
||||
host: z.string()
|
||||
});
|
||||
|
||||
export type TRelays = z.infer<typeof RelaysSchema>;
|
||||
export type TRelaysInsert = Omit<z.input<typeof RelaysSchema>, TImmutableDBKeys>;
|
||||
export type TRelaysUpdate = Partial<Omit<z.input<typeof RelaysSchema>, TImmutableDBKeys>>;
|
||||
25
backend/src/db/schemas/user-notifications.ts
Normal file
25
backend/src/db/schemas/user-notifications.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const UserNotificationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
type: z.string(),
|
||||
title: z.string(),
|
||||
body: z.string().nullable().optional(),
|
||||
link: z.string().nullable().optional(),
|
||||
isRead: z.boolean().default(false),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TUserNotifications = z.infer<typeof UserNotificationsSchema>;
|
||||
export type TUserNotificationsInsert = Omit<z.input<typeof UserNotificationsSchema>, TImmutableDBKeys>;
|
||||
export type TUserNotificationsUpdate = Partial<Omit<z.input<typeof UserNotificationsSchema>, TImmutableDBKeys>>;
|
||||
@@ -1,215 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AUDIT_LOG_STREAMS } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedAuditLogStreamSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create an Audit Log Stream.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url),
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.key),
|
||||
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.value)
|
||||
})
|
||||
.describe(AUDIT_LOG_STREAMS.CREATE.headers.desc)
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.create({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
url: req.body.url,
|
||||
headers: req.body.headers
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.UPDATE.id)
|
||||
}),
|
||||
body: z.object({
|
||||
url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url),
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.key),
|
||||
value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.value)
|
||||
})
|
||||
.describe(AUDIT_LOG_STREAMS.UPDATE.headers.desc)
|
||||
.array()
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.updateById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id,
|
||||
url: req.body.url,
|
||||
headers: req.body.headers
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.DELETE.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.deleteById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get an Audit Log Stream by ID.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string().describe(AUDIT_LOG_STREAMS.GET_BY_ID.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: SanitizedAuditLogStreamSchema.extend({
|
||||
headers: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStream = await server.services.auditLogStream.getById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List Audit Log Streams.",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStreams: SanitizedAuditLogStreamSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStreams = await server.services.auditLogStream.list({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { auditLogStreams };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums";
|
||||
import { TAuditLogStream } from "@app/ee/services/audit-log-stream/audit-log-stream-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAuditLogStreamEndpoints = <T extends TAuditLogStream>({
|
||||
server,
|
||||
provider,
|
||||
createSchema,
|
||||
updateSchema,
|
||||
sanitizedResponseSchema
|
||||
}: {
|
||||
server: FastifyZodProvider;
|
||||
provider: LogProvider;
|
||||
createSchema: z.ZodType<{
|
||||
credentials: T["credentials"];
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
credentials: T["credentials"];
|
||||
}>;
|
||||
sanitizedResponseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:logStreamId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
logStreamId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: sanitizedResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { logStreamId } = req.params;
|
||||
|
||||
const auditLogStream = await server.services.auditLogStream.getById(logStreamId, provider, req.permission);
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: createSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: sanitizedResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { credentials } = req.body;
|
||||
|
||||
const auditLogStream = await server.services.auditLogStream.create(
|
||||
{
|
||||
provider,
|
||||
credentials
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:logStreamId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
logStreamId: z.string().uuid()
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: sanitizedResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { logStreamId } = req.params;
|
||||
const { credentials } = req.body;
|
||||
|
||||
const auditLogStream = await server.services.auditLogStream.updateById(
|
||||
{
|
||||
logStreamId,
|
||||
provider,
|
||||
credentials
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:logStreamId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
logStreamId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStream: sanitizedResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { logStreamId } = req.params;
|
||||
|
||||
const auditLogStream = await server.services.auditLogStream.deleteById(logStreamId, provider, req.permission);
|
||||
|
||||
return { auditLogStream };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AzureProviderListItemSchema,
|
||||
SanitizedAzureProviderSchema
|
||||
} from "@app/ee/services/audit-log-stream/azure/azure-provider-schemas";
|
||||
import {
|
||||
CriblProviderListItemSchema,
|
||||
SanitizedCriblProviderSchema
|
||||
} from "@app/ee/services/audit-log-stream/cribl/cribl-provider-schemas";
|
||||
import {
|
||||
CustomProviderListItemSchema,
|
||||
SanitizedCustomProviderSchema
|
||||
} from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas";
|
||||
import {
|
||||
DatadogProviderListItemSchema,
|
||||
SanitizedDatadogProviderSchema
|
||||
} from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas";
|
||||
import {
|
||||
SanitizedSplunkProviderSchema,
|
||||
SplunkProviderListItemSchema
|
||||
} from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SanitizedAuditLogStreamSchema = z.union([
|
||||
SanitizedCustomProviderSchema,
|
||||
SanitizedDatadogProviderSchema,
|
||||
SanitizedSplunkProviderSchema,
|
||||
SanitizedAzureProviderSchema,
|
||||
SanitizedCriblProviderSchema
|
||||
]);
|
||||
|
||||
const ProviderOptionsSchema = z.discriminatedUnion("provider", [
|
||||
CustomProviderListItemSchema,
|
||||
DatadogProviderListItemSchema,
|
||||
SplunkProviderListItemSchema,
|
||||
AzureProviderListItemSchema,
|
||||
CriblProviderListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/options",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
providerOptions: ProviderOptionsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: () => {
|
||||
const providerOptions = server.services.auditLogStream.listProviderOptions();
|
||||
|
||||
return { providerOptions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogStreams: SanitizedAuditLogStreamSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogStreams = await server.services.auditLogStream.list(req.permission);
|
||||
|
||||
return { auditLogStreams };
|
||||
}
|
||||
});
|
||||
};
|
||||
79
backend/src/ee/routes/v1/audit-log-stream-routers/index.ts
Normal file
79
backend/src/ee/routes/v1/audit-log-stream-routers/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums";
|
||||
import {
|
||||
CreateAzureProviderLogStreamSchema,
|
||||
SanitizedAzureProviderSchema,
|
||||
UpdateAzureProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/azure/azure-provider-schemas";
|
||||
import {
|
||||
CreateCriblProviderLogStreamSchema,
|
||||
SanitizedCriblProviderSchema,
|
||||
UpdateCriblProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/cribl/cribl-provider-schemas";
|
||||
import {
|
||||
CreateCustomProviderLogStreamSchema,
|
||||
SanitizedCustomProviderSchema,
|
||||
UpdateCustomProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas";
|
||||
import {
|
||||
CreateDatadogProviderLogStreamSchema,
|
||||
SanitizedDatadogProviderSchema,
|
||||
UpdateDatadogProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas";
|
||||
import {
|
||||
CreateSplunkProviderLogStreamSchema,
|
||||
SanitizedSplunkProviderSchema,
|
||||
UpdateSplunkProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas";
|
||||
|
||||
import { registerAuditLogStreamEndpoints } from "./audit-log-stream-endpoints";
|
||||
|
||||
export * from "./audit-log-stream-router";
|
||||
|
||||
export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record<LogProvider, (server: FastifyZodProvider) => Promise<void>> =
|
||||
{
|
||||
[LogProvider.Azure]: async (server: FastifyZodProvider) => {
|
||||
registerAuditLogStreamEndpoints({
|
||||
server,
|
||||
provider: LogProvider.Azure,
|
||||
sanitizedResponseSchema: SanitizedAzureProviderSchema,
|
||||
createSchema: CreateAzureProviderLogStreamSchema,
|
||||
updateSchema: UpdateAzureProviderLogStreamSchema
|
||||
});
|
||||
},
|
||||
[LogProvider.Custom]: async (server: FastifyZodProvider) => {
|
||||
registerAuditLogStreamEndpoints({
|
||||
server,
|
||||
provider: LogProvider.Custom,
|
||||
sanitizedResponseSchema: SanitizedCustomProviderSchema,
|
||||
createSchema: CreateCustomProviderLogStreamSchema,
|
||||
updateSchema: UpdateCustomProviderLogStreamSchema
|
||||
});
|
||||
},
|
||||
[LogProvider.Datadog]: async (server: FastifyZodProvider) => {
|
||||
registerAuditLogStreamEndpoints({
|
||||
server,
|
||||
provider: LogProvider.Datadog,
|
||||
sanitizedResponseSchema: SanitizedDatadogProviderSchema,
|
||||
createSchema: CreateDatadogProviderLogStreamSchema,
|
||||
updateSchema: UpdateDatadogProviderLogStreamSchema
|
||||
});
|
||||
},
|
||||
[LogProvider.Splunk]: async (server: FastifyZodProvider) => {
|
||||
registerAuditLogStreamEndpoints({
|
||||
server,
|
||||
provider: LogProvider.Splunk,
|
||||
sanitizedResponseSchema: SanitizedSplunkProviderSchema,
|
||||
createSchema: CreateSplunkProviderLogStreamSchema,
|
||||
updateSchema: UpdateSplunkProviderLogStreamSchema
|
||||
});
|
||||
},
|
||||
[LogProvider.Cribl]: async (server: FastifyZodProvider) => {
|
||||
registerAuditLogStreamEndpoints({
|
||||
server,
|
||||
provider: LogProvider.Cribl,
|
||||
sanitizedResponseSchema: SanitizedCriblProviderSchema,
|
||||
createSchema: CreateCriblProviderLogStreamSchema,
|
||||
updateSchema: UpdateCriblProviderLogStreamSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
287
backend/src/ee/routes/v1/deprecated-project-role-router.ts
Normal file
287
backend/src/ee/routes/v1/deprecated-project-role-router.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import {
|
||||
backfillPermissionV1SchemaToV2Schema,
|
||||
ProjectPermissionV1Schema
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
|
||||
|
||||
export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:projectSlug/roles",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create a project role",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: slugSchema({ max: 64 })
|
||||
.refine(
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.describe(PROJECT_ROLE.CREATE.slug),
|
||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
filter: {
|
||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
||||
projectSlug: req.params.projectSlug
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:projectSlug/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update a project role",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: slugSchema({ max: 64 })
|
||||
.refine(
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||
.optional(),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions
|
||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
: undefined
|
||||
}
|
||||
});
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:projectSlug/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete a project role",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.projectRole.deleteRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectSlug/roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List project role",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.projectRole.listRoles({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
filter: {
|
||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
||||
projectSlug: req.params.projectSlug
|
||||
}
|
||||
});
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectSlug/roles/slug/:slug",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
|
||||
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1.omit({ version: true })
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.projectRole.getRoleBySlug({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
filter: {
|
||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
||||
projectSlug: req.params.projectSlug
|
||||
},
|
||||
roleSlug: req.params.slug
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/permissions",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
data: z.object({
|
||||
membership: z.object({
|
||||
id: z.string(),
|
||||
roles: z
|
||||
.object({
|
||||
role: z.string()
|
||||
})
|
||||
.array()
|
||||
}),
|
||||
assumedPrivilegeDetails: z
|
||||
.object({
|
||||
actorId: z.string(),
|
||||
actorType: z.string(),
|
||||
actorName: z.string(),
|
||||
actorEmail: z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
permissions: z.any().array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission(
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
return {
|
||||
data: {
|
||||
permissions,
|
||||
membership,
|
||||
assumedPrivilegeDetails
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
195
backend/src/ee/routes/v1/deprecated-project-router.ts
Normal file
195
backend/src/ee/routes/v1/deprecated-project-router.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/secret-snapshots",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.Projects],
|
||||
description: "Return project secret snapshots ids",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.projectId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(PROJECTS.GET_SNAPSHOTS.path),
|
||||
offset: z.coerce.number().default(0).describe(PROJECTS.GET_SNAPSHOTS.offset),
|
||||
limit: z.coerce.number().default(20).describe(PROJECTS.GET_SNAPSHOTS.limit)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretSnapshots: SecretSnapshotsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const secretSnapshots = await server.services.snapshot.listSnapshots({
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.query
|
||||
});
|
||||
return { secretSnapshots };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/secret-snapshots/count",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
count: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const count = await server.services.snapshot.projectSecretSnapshotCount({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
});
|
||||
return { count };
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Daniel: This endpoint is no longer is use.
|
||||
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
|
||||
*
|
||||
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
|
||||
*/
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Return audit logs",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||
}),
|
||||
querystring: z
|
||||
.object({
|
||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
||||
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||
limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
||||
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
||||
})
|
||||
.superRefine((el, ctx) => {
|
||||
if (el.endDate && el.startDate) {
|
||||
const startDate = new Date(el.startDate);
|
||||
const endDate = new Date(el.endDate);
|
||||
const maxAllowedDate = new Date(startDate);
|
||||
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
|
||||
if (endDate < startDate) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["endDate"],
|
||||
message: "End date cannot be before start date"
|
||||
});
|
||||
}
|
||||
if (endDate > maxAllowedDate) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["endDate"],
|
||||
message: "Dates must be within 3 months"
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogs: AuditLogsSchema.omit({
|
||||
eventMetadata: true,
|
||||
eventType: true,
|
||||
actor: true,
|
||||
actorMetadata: true
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
}),
|
||||
actor: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
})
|
||||
})
|
||||
)
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
|
||||
filter: {
|
||||
...req.query,
|
||||
projectId: req.params.workspaceId,
|
||||
endDate: req.query.endDate || new Date().toISOString(),
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActorId: req.query.actor,
|
||||
eventType: req.query.eventType ? [req.query.eventType] : undefined
|
||||
}
|
||||
});
|
||||
return { auditLogs };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,293 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerDeprecatedSecretApprovalPolicyRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
workspaceId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string().optional(),
|
||||
environments: z.string().array().optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.min(1, { message: "Secret path cannot be empty" })
|
||||
.transform((val) => removeTrailingSlash(val)),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({
|
||||
type: z.literal(ApproverType.User),
|
||||
id: z.string().optional(),
|
||||
username: z.string().optional()
|
||||
})
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" })
|
||||
.max(100, "Cannot have more than 100 approvers"),
|
||||
bypassers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||
z.object({
|
||||
type: z.literal(BypasserType.User),
|
||||
id: z.string().optional(),
|
||||
username: z.string().optional()
|
||||
})
|
||||
])
|
||||
.array()
|
||||
.max(100, "Cannot have more than 100 bypassers")
|
||||
.optional(),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
})
|
||||
.refine((data) => data.environment || data.environments, "At least one environment should be provided"),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.body.workspaceId,
|
||||
...req.body,
|
||||
name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`,
|
||||
enforcementLevel: req.body.enforcementLevel
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "PATCH",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().optional(),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" })
|
||||
.max(100, "Cannot have more than 100 approvers"),
|
||||
bypassers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.max(100, "Cannot have more than 100 bypassers")
|
||||
.optional(),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Secret path cannot be empty" })
|
||||
.optional()
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : undefined)),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
|
||||
allowedSelfApprovals: z.boolean().default(true),
|
||||
environments: z.array(z.string()).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
secretPolicyId: req.params.sapId
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "DELETE",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
secretPolicyId: req.params.sapId
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: sapPubSchema
|
||||
.extend({
|
||||
approvers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType)
|
||||
})
|
||||
.array(),
|
||||
bypassers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(BypasserType)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema.extend({
|
||||
approvers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType),
|
||||
username: z.string().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
bypassers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(BypasserType),
|
||||
username: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/board",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
policy: sapPubSchema
|
||||
.extend({
|
||||
userApprovers: z.object({ userId: z.string().nullable().optional() }).array()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId,
|
||||
...req.query
|
||||
});
|
||||
return { policy };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -84,7 +84,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dynamicSecret: SanitizedDynamicSecretSchema
|
||||
dynamicSecret: SanitizedDynamicSecretSchema.extend({
|
||||
inputs: z.unknown()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -151,7 +153,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dynamicSecret: SanitizedDynamicSecretSchema
|
||||
dynamicSecret: SanitizedDynamicSecretSchema.extend({
|
||||
inputs: z.unknown()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,8 +3,11 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-templat
|
||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||
import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers";
|
||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||
import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-router";
|
||||
import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
|
||||
import { registerDeprecatedSecretApprovalPolicyRouter } from "./deprecated-secret-approval-policy-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
@@ -24,9 +27,9 @@ import { registerPITRouter } from "./pit-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
import { registerProjectRouter } from "./project-router";
|
||||
import { registerRateLimitRouter } from "./rate-limit-router";
|
||||
import { registerRelayRouter } from "./relay-router";
|
||||
import { registerSamlRouter } from "./saml-router";
|
||||
import { registerScimRouter } from "./scim-router";
|
||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
||||
@@ -46,18 +49,29 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
// org role starts with organization
|
||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||
await server.register(registerLicenseRouter, { prefix: "/organizations" });
|
||||
|
||||
// depreciated in favour of infisical workspace
|
||||
await server.register(
|
||||
async (projectRouter) => {
|
||||
await projectRouter.register(registerProjectRoleRouter);
|
||||
await projectRouter.register(registerProjectRouter);
|
||||
await projectRouter.register(registerTrustedIpRouter);
|
||||
await projectRouter.register(registerAssumePrivilegeRouter);
|
||||
await projectRouter.register(registerDeprecatedProjectRoleRouter);
|
||||
await projectRouter.register(registerDeprecatedProjectRouter);
|
||||
},
|
||||
{ prefix: "/workspace" }
|
||||
);
|
||||
|
||||
await server.register(
|
||||
async (projectRouter) => {
|
||||
await projectRouter.register(registerProjectRoleRouter);
|
||||
await projectRouter.register(registerTrustedIpRouter);
|
||||
await projectRouter.register(registerAssumePrivilegeRouter);
|
||||
await projectRouter.register(registerProjectRouter);
|
||||
},
|
||||
{ prefix: "/projects" }
|
||||
);
|
||||
|
||||
await server.register(registerSnapshotRouter, { prefix: "/secret-snapshot" });
|
||||
await server.register(registerPITRouter, { prefix: "/pit" });
|
||||
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||
await server.register(registerDeprecatedSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||
await server.register(registerSecretApprovalRequestRouter, {
|
||||
prefix: "/secret-approval-requests"
|
||||
});
|
||||
@@ -79,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||
await server.register(registerRelayRouter, { prefix: "/relays" });
|
||||
await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" });
|
||||
|
||||
await server.register(
|
||||
@@ -114,7 +129,21 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
||||
|
||||
await server.register(
|
||||
async (auditLogStreamRouter) => {
|
||||
await auditLogStreamRouter.register(registerAuditLogStreamRouter);
|
||||
|
||||
// Provider-specific endpoints
|
||||
await Promise.all(
|
||||
Object.entries(AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP).map(([provider, router]) =>
|
||||
auditLogStreamRouter.register(router, { prefix: `/${provider}` })
|
||||
)
|
||||
);
|
||||
},
|
||||
{ prefix: "/audit-log-streams" }
|
||||
);
|
||||
|
||||
await server.register(registerUserAdditionalPrivilegeRouter, { prefix: "/user-project-additional-privilege" });
|
||||
await server.register(
|
||||
async (privilegeRouter) => {
|
||||
|
||||
@@ -43,6 +43,12 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
querystring: z.object({
|
||||
refreshCache: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ plan: z.any() })
|
||||
}
|
||||
@@ -54,7 +60,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
orgId: req.params.organizationId,
|
||||
refreshCache: req.query.refreshCache
|
||||
});
|
||||
return { plan };
|
||||
}
|
||||
|
||||
@@ -2,26 +2,26 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import {
|
||||
backfillPermissionV1SchemaToV2Schema,
|
||||
ProjectPermissionV1Schema
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
|
||||
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
|
||||
|
||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:projectSlug/roles",
|
||||
url: "/:projectId/roles",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.ProjectRoles],
|
||||
description: "Create a project role",
|
||||
security: [
|
||||
{
|
||||
@@ -29,10 +29,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
||||
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: slugSchema({ max: 64 })
|
||||
slug: slugSchema({ min: 1, max: 64 })
|
||||
.refine(
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
@@ -40,11 +40,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(PROJECT_ROLE.CREATE.slug),
|
||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.describe(PROJECT_ROLE.CREATE.permissions)
|
||||
.refine(checkForInvalidPermissionCombination)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1
|
||||
role: SanitizedRoleSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -56,26 +58,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
filter: {
|
||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
||||
projectSlug: req.params.projectSlug
|
||||
type: ProjectRoleServiceIdentifierType.ID,
|
||||
projectId: req.params.projectId
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:projectSlug/roles/:roleId",
|
||||
url: "/:projectId/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.ProjectRoles],
|
||||
description: "Update a project role",
|
||||
security: [
|
||||
{
|
||||
@@ -83,24 +86,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
|
||||
projectId: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectId),
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: slugSchema({ max: 64 })
|
||||
slug: slugSchema({ min: 1, max: 64 })
|
||||
.refine(
|
||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||
"Please choose a different slug, the slug you have entered is reserved"
|
||||
)
|
||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||
.optional(),
|
||||
.optional()
|
||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
permissions: ProjectPermissionV2Schema.array()
|
||||
.describe(PROJECT_ROLE.UPDATE.permissions)
|
||||
.optional()
|
||||
.superRefine(checkForInvalidPermissionCombination)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1
|
||||
role: SanitizedRoleSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -114,9 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions
|
||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
: undefined
|
||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||
}
|
||||
});
|
||||
return { role };
|
||||
@@ -125,11 +129,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:projectSlug/roles/:roleId",
|
||||
url: "/:projectId/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.ProjectRoles],
|
||||
description: "Delete a project role",
|
||||
security: [
|
||||
{
|
||||
@@ -137,12 +143,12 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
|
||||
projectId: z.string().trim().describe(PROJECT_ROLE.DELETE.projectId),
|
||||
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1
|
||||
role: SanitizedRoleSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -161,11 +167,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectSlug/roles",
|
||||
url: "/:projectId/roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.ProjectRoles],
|
||||
description: "List project role",
|
||||
security: [
|
||||
{
|
||||
@@ -173,7 +181,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
|
||||
projectId: z.string().trim().describe(PROJECT_ROLE.LIST.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -189,8 +197,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
filter: {
|
||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
||||
projectSlug: req.params.projectSlug
|
||||
type: ProjectRoleServiceIdentifierType.ID,
|
||||
projectId: req.params.projectId
|
||||
}
|
||||
});
|
||||
return { roles };
|
||||
@@ -199,18 +207,20 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectSlug/roles/slug/:slug",
|
||||
url: "/:projectId/roles/slug/:roleSlug",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.ProjectRoles],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
|
||||
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
|
||||
projectId: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectId),
|
||||
roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: SanitizedRoleSchemaV1.omit({ version: true })
|
||||
role: SanitizedRoleSchema.omit({ version: true })
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -222,12 +232,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
filter: {
|
||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
||||
projectSlug: req.params.projectSlug
|
||||
type: ProjectRoleServiceIdentifierType.ID,
|
||||
projectId: req.params.projectId
|
||||
},
|
||||
roleSlug: req.params.slug
|
||||
roleSlug: req.params.roleSlug
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { SecretSnapshotsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -12,7 +12,7 @@ import { KmsType } from "@app/services/kms/kms-types";
|
||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/secret-snapshots",
|
||||
url: "/:projectId/secret-snapshots",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -26,7 +26,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.workspaceId)
|
||||
projectId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.projectId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment),
|
||||
@@ -47,7 +47,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
...req.query
|
||||
});
|
||||
return { secretSnapshots };
|
||||
@@ -56,13 +56,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/secret-snapshots/count",
|
||||
url: "/:projectId/secret-snapshots/count",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
@@ -81,7 +81,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
});
|
||||
@@ -89,140 +89,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Daniel: This endpoint is no longer is use.
|
||||
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
|
||||
*
|
||||
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
|
||||
*/
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Return audit logs",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||
}),
|
||||
querystring: z
|
||||
.object({
|
||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
||||
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||
limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
||||
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
||||
})
|
||||
.superRefine((el, ctx) => {
|
||||
if (el.endDate && el.startDate) {
|
||||
const startDate = new Date(el.startDate);
|
||||
const endDate = new Date(el.endDate);
|
||||
const maxAllowedDate = new Date(startDate);
|
||||
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
|
||||
if (endDate < startDate) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["endDate"],
|
||||
message: "End date cannot be before start date"
|
||||
});
|
||||
}
|
||||
if (endDate > maxAllowedDate) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["endDate"],
|
||||
message: "Dates must be within 3 months"
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogs: AuditLogsSchema.omit({
|
||||
eventMetadata: true,
|
||||
eventType: true,
|
||||
actor: true,
|
||||
actorMetadata: true
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
}),
|
||||
actor: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
})
|
||||
})
|
||||
)
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
|
||||
filter: {
|
||||
...req.query,
|
||||
projectId: req.params.workspaceId,
|
||||
endDate: req.query.endDate || new Date().toISOString(),
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActorId: req.query.actor,
|
||||
eventType: req.query.eventType ? [req.query.eventType] : undefined
|
||||
}
|
||||
});
|
||||
return { auditLogs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs/filters/actors",
|
||||
url: "/:projectId/kms",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
actors: z.string().array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async () => ({ actors: [] })
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/kms",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -241,7 +116,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
|
||||
return kmsKey;
|
||||
@@ -250,13 +125,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:workspaceId/kms",
|
||||
url: "/:projectId/kms",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
kms: z.discriminatedUnion("type", [
|
||||
@@ -281,13 +156,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_KMS,
|
||||
metadata: {
|
||||
@@ -307,13 +182,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/kms/backup",
|
||||
url: "/:projectId/kms/backup",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -328,12 +203,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_KMS_BACKUP,
|
||||
metadata: {}
|
||||
@@ -346,13 +221,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/kms/backup",
|
||||
url: "/:projectId/kms/backup",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
backup: z.string().min(1)
|
||||
@@ -374,13 +249,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
backup: req.body.backup
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
event: {
|
||||
type: EventType.LOAD_PROJECT_KMS_BACKUP,
|
||||
metadata: {}
|
||||
@@ -393,13 +268,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/migrate-v3",
|
||||
url: "/:projectId/migrate-v3",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
|
||||
response: {
|
||||
@@ -415,7 +290,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
|
||||
return migration;
|
||||
|
||||
103
backend/src/ee/routes/v1/relay-router.ts
Normal file
103
backend/src/ee/routes/v1/relay-router.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerRelayRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/register-instance-relay",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
host: z.string(),
|
||||
name: slugSchema({ min: 1, max: 32, field: "name" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
pki: z.object({
|
||||
serverCertificate: z.string(),
|
||||
serverPrivateKey: z.string(),
|
||||
clientCertificateChain: z.string()
|
||||
}),
|
||||
ssh: z.object({
|
||||
serverCertificate: z.string(),
|
||||
serverPrivateKey: z.string(),
|
||||
clientCAPublicKey: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, _, next) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (appCfg.RELAY_AUTH_SECRET && authHeader) {
|
||||
const expectedHeader = `Bearer ${appCfg.RELAY_AUTH_SECRET}`;
|
||||
if (
|
||||
authHeader.length === expectedHeader.length &&
|
||||
crypto.nativeCrypto.timingSafeEqual(Buffer.from(authHeader), Buffer.from(expectedHeader))
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid relay auth secret"
|
||||
});
|
||||
},
|
||||
handler: async (req) => {
|
||||
return server.services.relay.registerRelay({
|
||||
...req.body
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/register-org-relay",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
host: z.string(),
|
||||
name: slugSchema({ min: 1, max: 32, field: "name" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
pki: z.object({
|
||||
serverCertificate: z.string(),
|
||||
serverPrivateKey: z.string(),
|
||||
clientCertificateChain: z.string()
|
||||
}),
|
||||
ssh: z.object({
|
||||
serverCertificate: z.string(),
|
||||
serverPrivateKey: z.string(),
|
||||
clientCAPublicKey: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
throw new BadRequestError({
|
||||
message: "Org relay registration is not yet supported"
|
||||
});
|
||||
|
||||
return server.services.relay.registerRelay({
|
||||
...req.body,
|
||||
identityId: req.permission.id,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim().optional(),
|
||||
committer: z.string().trim().optional(),
|
||||
search: z.string().trim().optional(),
|
||||
@@ -80,7 +80,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query,
|
||||
projectId: req.query.workspaceId
|
||||
projectId: req.query.projectId
|
||||
});
|
||||
return { approvals, totalCount };
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
policyId: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
@@ -113,7 +113,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId,
|
||||
projectId: req.query.projectId,
|
||||
policyId: req.query.policyId
|
||||
});
|
||||
return { approvals };
|
||||
|
||||
@@ -9,13 +9,13 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/trusted-ips",
|
||||
url: "/:projectId/trusted-ips",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -27,7 +27,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const trustedIps = await server.services.trustedIp.listIpsByProjectId({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId
|
||||
@@ -38,13 +38,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/trusted-ips",
|
||||
url: "/:projectId/trusted-ips",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
ipAddress: z.string().trim(),
|
||||
@@ -61,7 +61,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { trustedIp, project } = await server.services.trustedIp.addProjectIp({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
@@ -86,13 +86,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
||||
url: "/:projectId/trusted-ips/:trustedIpId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
trustedIpId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
@@ -108,7 +108,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { trustedIp, project } = await server.services.trustedIp.updateProjectIp({
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -135,13 +135,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
||||
url: "/:projectId/trusted-ips/:trustedIpId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
trustedIpId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
@@ -153,7 +153,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { trustedIp, project } = await server.services.trustedIp.deleteProjectIp({
|
||||
projectId: req.params.workspaceId,
|
||||
projectId: req.params.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
|
||||
|
||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:projectId/roles",
|
||||
133
backend/src/ee/routes/v2/gateway-router.ts
Normal file
133
backend/src/ee/routes/v2/gateway-router.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import z from "zod";
|
||||
|
||||
import { GatewaysV2Schema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SanitizedGatewayV2Schema = GatewaysV2Schema.pick({
|
||||
id: true,
|
||||
identityId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
heartbeat: true
|
||||
});
|
||||
|
||||
export const registerGatewayV2Router = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
schema: {
|
||||
body: z.object({
|
||||
relayName: slugSchema({ min: 1, max: 32, field: "relayName" }),
|
||||
name: slugSchema({ min: 1, max: 32, field: "name" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gatewayId: z.string(),
|
||||
relayHost: z.string(),
|
||||
pki: z.object({
|
||||
serverCertificate: z.string(),
|
||||
serverPrivateKey: z.string(),
|
||||
clientCertificateChain: z.string()
|
||||
}),
|
||||
ssh: z.object({
|
||||
clientCertificate: z.string(),
|
||||
clientPrivateKey: z.string(),
|
||||
serverCAPublicKey: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gatewayV2.registerGateway({
|
||||
orgId: req.permission.orgId,
|
||||
relayName: req.body.relayName,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
name: req.body.name
|
||||
});
|
||||
|
||||
return gateway;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/heartbeat",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.gatewayV2.heartbeat({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
|
||||
return { message: "Successfully triggered heartbeat" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
response: {
|
||||
200: SanitizedGatewayV2Schema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
}).array()
|
||||
}
|
||||
},
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const gateways = await server.services.gatewayV2.listGateways({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
|
||||
return gateways;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: SanitizedGatewayV2Schema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gatewayV2.deleteGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id
|
||||
});
|
||||
return gateway;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -7,14 +7,16 @@ import {
|
||||
SECRET_SCANNING_REGISTER_ROUTER_MAP
|
||||
} from "@app/ee/routes/v2/secret-scanning-v2-routers";
|
||||
|
||||
import { registerDeprecatedProjectRoleRouter } from "./deprecated-project-role-router";
|
||||
import { registerGatewayV2Router } from "./gateway-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||
|
||||
export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||
// org role starts with organization
|
||||
await server.register(
|
||||
async (projectRouter) => {
|
||||
await projectRouter.register(registerProjectRoleRouter);
|
||||
// this has been depreciated and moved to /api/v1/projects
|
||||
await projectRouter.register(registerDeprecatedProjectRoleRouter);
|
||||
},
|
||||
{ prefix: "/workspace" }
|
||||
);
|
||||
@@ -23,6 +25,10 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||
prefix: "/identity-project-additional-privilege"
|
||||
});
|
||||
|
||||
await server.register(registerGatewayV2Router, { prefix: "/gateways" });
|
||||
|
||||
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||
|
||||
await server.register(
|
||||
async (secretRotationV2Router) => {
|
||||
// register generic secret rotation endpoints
|
||||
|
||||
@@ -19,7 +19,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
workspaceId: z.string(),
|
||||
projectId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string().optional(),
|
||||
environments: z.string().array().optional(),
|
||||
@@ -69,7 +69,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.body.workspaceId,
|
||||
...req.body,
|
||||
name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`,
|
||||
enforcementLevel: req.body.enforcementLevel
|
||||
@@ -174,7 +173,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
projectId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -204,7 +203,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
projectId: req.query.projectId
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
@@ -263,7 +262,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().transform(removeTrailingSlash)
|
||||
}),
|
||||
@@ -284,7 +283,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId,
|
||||
...req.query
|
||||
});
|
||||
return { policy };
|
||||
@@ -20,6 +20,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TNotificationServiceFactory } from "../../../services/notification/notification-service";
|
||||
import { NotificationType } from "../../../services/notification/notification-types";
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
@@ -67,6 +69,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export const accessApprovalRequestServiceFactory = ({
|
||||
@@ -84,7 +87,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
kmsService,
|
||||
microsoftTeamsService,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
projectSlackConfigDAL
|
||||
projectSlackConfigDAL,
|
||||
notificationService
|
||||
}: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => {
|
||||
const $getEnvironmentFromPermissions = (permissions: unknown): string | null => {
|
||||
if (!Array.isArray(permissions) || permissions.length === 0) {
|
||||
@@ -245,7 +249,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
);
|
||||
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`;
|
||||
const approvalPath = `/projects/secret-management/${project.id}/approval`;
|
||||
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
@@ -274,6 +279,17 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
approverUsers.map((approver) => ({
|
||||
userId: approver.id,
|
||||
orgId: actorOrgId,
|
||||
type: NotificationType.ACCESS_APPROVAL_REQUEST,
|
||||
title: "Access Approval Request",
|
||||
body: `**${requesterFullName}** (${requestedByUser.email}) has requested ${isTemporary ? "temporary" : "permanent"} access to **${secretPath}** in the **${envSlug}** environment for project **${project.name}**.`,
|
||||
link: approvalPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request",
|
||||
@@ -391,7 +407,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`;
|
||||
const approvalUrl = `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`;
|
||||
const approvalPath = `/projects/secret-management/${project.id}/approval`;
|
||||
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||
|
||||
await triggerWorkflowIntegrationNotification({
|
||||
input: {
|
||||
@@ -422,27 +439,44 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: policy.approvers
|
||||
.filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id)
|
||||
.map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request Updated",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary: true,
|
||||
expiresIn: msFn(ms(temporaryRange || ""), { long: true }),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
editNote,
|
||||
editorFullName,
|
||||
editorEmail: editedByUser.email
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequestUpdated
|
||||
});
|
||||
await notificationService.createUserNotifications(
|
||||
policy.approvers
|
||||
.filter((approver) => Boolean(approver.userId) && approver.userId !== editedByUser.id)
|
||||
.map((approver) => ({
|
||||
userId: approver.userId!,
|
||||
orgId: actorOrgId,
|
||||
type: NotificationType.ACCESS_APPROVAL_REQUEST_UPDATED,
|
||||
title: "Access Approval Request Updated",
|
||||
body: `**${editorFullName}** (${editedByUser.email}) has updated the access request submitted by **${requesterFullName}** (${requestedByUser.email}) for **${secretPath}** in the **${envSlug}** environment for project **${project.name}**.`,
|
||||
link: approvalPath
|
||||
}))
|
||||
);
|
||||
|
||||
const recipients = policy.approvers
|
||||
.filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id)
|
||||
.map((approver) => approver.email!);
|
||||
|
||||
if (recipients.length > 0) {
|
||||
await smtpService.sendMail({
|
||||
recipients,
|
||||
subjectLine: "Access Approval Request Updated",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary: true,
|
||||
expiresIn: msFn(ms(temporaryRange || ""), { long: true }),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
editNote,
|
||||
editorFullName,
|
||||
editorEmail: editedByUser.email
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequestUpdated
|
||||
});
|
||||
}
|
||||
|
||||
return approvalRequest;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum LogProvider {
|
||||
Azure = "azure",
|
||||
Cribl = "cribl",
|
||||
Custom = "custom",
|
||||
Datadog = "datadog",
|
||||
Splunk = "splunk"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { LogProvider } from "./audit-log-stream-enums";
|
||||
import { TAuditLogStreamCredentials, TLogStreamFactory } from "./audit-log-stream-types";
|
||||
import { AzureProviderFactory } from "./azure/azure-provider-factory";
|
||||
import { CriblProviderFactory } from "./cribl/cribl-provider-factory";
|
||||
import { CustomProviderFactory } from "./custom/custom-provider-factory";
|
||||
import { DatadogProviderFactory } from "./datadog/datadog-provider-factory";
|
||||
import { SplunkProviderFactory } from "./splunk/splunk-provider-factory";
|
||||
|
||||
type TLogStreamFactoryImplementation = TLogStreamFactory<TAuditLogStreamCredentials>;
|
||||
|
||||
export const LOG_STREAM_FACTORY_MAP: Record<LogProvider, TLogStreamFactoryImplementation> = {
|
||||
[LogProvider.Azure]: AzureProviderFactory as TLogStreamFactoryImplementation,
|
||||
[LogProvider.Datadog]: DatadogProviderFactory as TLogStreamFactoryImplementation,
|
||||
[LogProvider.Splunk]: SplunkProviderFactory as TLogStreamFactoryImplementation,
|
||||
[LogProvider.Custom]: CustomProviderFactory as TLogStreamFactoryImplementation,
|
||||
[LogProvider.Cribl]: CriblProviderFactory as TLogStreamFactoryImplementation
|
||||
};
|
||||
@@ -1,21 +1,76 @@
|
||||
export function providerSpecificPayload(url: string) {
|
||||
const { hostname } = new URL(url);
|
||||
import { TAuditLogStreams } from "@app/db/schemas";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
const payload: Record<string, string> = {};
|
||||
import { TAuditLogStream, TAuditLogStreamCredentials } from "./audit-log-stream-types";
|
||||
import { getAzureProviderListItem } from "./azure/azure-provider-fns";
|
||||
import { getCriblProviderListItem } from "./cribl/cribl-provider-fns";
|
||||
import { getCustomProviderListItem } from "./custom/custom-provider-fns";
|
||||
import { getDatadogProviderListItem } from "./datadog/datadog-provider-fns";
|
||||
import { getSplunkProviderListItem } from "./splunk/splunk-provider-fns";
|
||||
|
||||
switch (hostname) {
|
||||
case "http-intake.logs.datadoghq.com":
|
||||
case "http-intake.logs.us3.datadoghq.com":
|
||||
case "http-intake.logs.us5.datadoghq.com":
|
||||
case "http-intake.logs.datadoghq.eu":
|
||||
case "http-intake.logs.ap1.datadoghq.com":
|
||||
case "http-intake.logs.ddog-gov.com":
|
||||
payload.ddsource = "infisical";
|
||||
payload.service = "audit-logs";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
export const listProviderOptions = () => {
|
||||
return [
|
||||
getDatadogProviderListItem(),
|
||||
getSplunkProviderListItem(),
|
||||
getCustomProviderListItem(),
|
||||
getAzureProviderListItem(),
|
||||
getCriblProviderListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
export const encryptLogStreamCredentials = async ({
|
||||
orgId,
|
||||
credentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
credentials: TAuditLogStreamCredentials;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(credentials))
|
||||
});
|
||||
|
||||
return encryptedCredentialsBlob;
|
||||
};
|
||||
|
||||
export const decryptLogStreamCredentials = async ({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
encryptedCredentials: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedCredentials
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TAuditLogStreamCredentials;
|
||||
};
|
||||
|
||||
export const decryptLogStream = async (
|
||||
logStream: TAuditLogStreams,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
return {
|
||||
...logStream,
|
||||
credentials: await decryptLogStreamCredentials({
|
||||
encryptedCredentials: logStream.encryptedCredentials,
|
||||
orgId: logStream.orgId,
|
||||
kmsService
|
||||
})
|
||||
} as TAuditLogStream;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AuditLogStreamsSchema } from "@app/db/schemas";
|
||||
|
||||
export const BaseProviderSchema = AuditLogStreamsSchema.omit({
|
||||
encryptedCredentials: true,
|
||||
provider: true,
|
||||
|
||||
// Old "archived" values
|
||||
encryptedHeadersAlgorithm: true,
|
||||
encryptedHeadersCiphertext: true,
|
||||
encryptedHeadersIV: true,
|
||||
encryptedHeadersKeyEncoding: true,
|
||||
encryptedHeadersTag: true,
|
||||
url: true
|
||||
});
|
||||
@@ -1,242 +1,252 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAuditLogs } from "@app/db/schemas";
|
||||
import {
|
||||
decryptLogStream,
|
||||
decryptLogStreamCredentials,
|
||||
encryptLogStreamCredentials,
|
||||
listProviderOptions
|
||||
} from "@app/ee/services/audit-log-stream/audit-log-stream-fns";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
|
||||
import { providerSpecificPayload } from "./audit-log-stream-fns";
|
||||
import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types";
|
||||
import { LogProvider } from "./audit-log-stream-enums";
|
||||
import { LOG_STREAM_FACTORY_MAP } from "./audit-log-stream-factory";
|
||||
import { TAuditLogStream, TCreateAuditLogStreamDTO, TUpdateAuditLogStreamDTO } from "./audit-log-stream-types";
|
||||
import { TCustomProviderCredentials } from "./custom/custom-provider-types";
|
||||
|
||||
type TAuditLogStreamServiceFactoryDep = {
|
||||
export type TAuditLogStreamServiceFactoryDep = {
|
||||
auditLogStreamDAL: TAuditLogStreamDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TAuditLogStreamServiceFactory = ReturnType<typeof auditLogStreamServiceFactory>;
|
||||
|
||||
export const auditLogStreamServiceFactory = ({
|
||||
auditLogStreamDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TAuditLogStreamServiceFactoryDep): TAuditLogStreamServiceFactory => {
|
||||
const create: TAuditLogStreamServiceFactory["create"] = async ({
|
||||
url,
|
||||
actor,
|
||||
headers = [],
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
licenseService,
|
||||
kmsService
|
||||
}: TAuditLogStreamServiceFactoryDep) => {
|
||||
const create = async ({ provider, credentials }: TCreateAuditLogStreamDTO, actor: OrgServiceActor) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
if (!plan.auditLogStreams) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
|
||||
message: "Failed to create Audit Log Stream: Plan restriction. Upgrade plan to continue."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actor.orgId });
|
||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams."
|
||||
message: "Failed to create Audit Log Stream: Plan limit reached. Contact Infisical to increase quota."
|
||||
});
|
||||
}
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
if (headers.length)
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
const factory = LOG_STREAM_FACTORY_MAP[provider]();
|
||||
const validatedCredentials = await factory.validateCredentials({ credentials });
|
||||
|
||||
await request
|
||||
.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` });
|
||||
});
|
||||
const encryptedCredentials = await encryptLogStreamCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const encryptedHeaders = headers
|
||||
? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers))
|
||||
: undefined;
|
||||
const logStream = await auditLogStreamDAL.create({
|
||||
orgId: actorOrgId,
|
||||
url,
|
||||
...(encryptedHeaders
|
||||
? {
|
||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||
encryptedHeadersIV: encryptedHeaders.iv,
|
||||
encryptedHeadersTag: encryptedHeaders.tag,
|
||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||
}
|
||||
: {})
|
||||
orgId: actor.orgId,
|
||||
provider,
|
||||
encryptedCredentials
|
||||
});
|
||||
return logStream;
|
||||
|
||||
return { ...logStream, credentials: validatedCredentials } as TAuditLogStream;
|
||||
};
|
||||
|
||||
const updateById: TAuditLogStreamServiceFactory["updateById"] = async ({
|
||||
id,
|
||||
url,
|
||||
actor,
|
||||
headers = [],
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams)
|
||||
const updateById = async (
|
||||
{ logStreamId, provider, credentials }: TUpdateAuditLogStreamDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
if (!plan.auditLogStreams) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update audit log streams due to plan restriction. Upgrade plan to create group."
|
||||
message: "Failed to update Audit Log Stream: Plan restriction. Upgrade plan to continue."
|
||||
});
|
||||
}
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
||||
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
logStream.orgId
|
||||
);
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
const appCfg = getConfig();
|
||||
if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
if (headers.length)
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
const finalCredentials = { ...credentials };
|
||||
|
||||
await request
|
||||
.post(
|
||||
url || logStream.url,
|
||||
{ ...providerSpecificPayload(url || logStream.url), ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
|
||||
});
|
||||
// For the "Custom" provider, we must handle masked header values ('******').
|
||||
// These are placeholders from the frontend for secrets that haven't been changed.
|
||||
// We need to replace them with the original, unmasked values from the database.
|
||||
if (
|
||||
provider === LogProvider.Custom &&
|
||||
"headers" in finalCredentials &&
|
||||
Array.isArray(finalCredentials.headers) &&
|
||||
finalCredentials.headers.some((header) => header.value === "******")
|
||||
) {
|
||||
const decryptedOldCredentials = (await decryptLogStreamCredentials({
|
||||
encryptedCredentials: logStream.encryptedCredentials,
|
||||
orgId: logStream.orgId,
|
||||
kmsService
|
||||
})) as TCustomProviderCredentials;
|
||||
|
||||
const encryptedHeaders = headers
|
||||
? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers))
|
||||
: undefined;
|
||||
const updatedLogStream = await auditLogStreamDAL.updateById(id, {
|
||||
url,
|
||||
...(encryptedHeaders
|
||||
? {
|
||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||
encryptedHeadersIV: encryptedHeaders.iv,
|
||||
encryptedHeadersTag: encryptedHeaders.tag,
|
||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||
const oldHeadersMap = decryptedOldCredentials.headers.reduce<Record<string, string>>((acc, header) => {
|
||||
acc[header.key] = header.value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const finalHeaders: { key: string; value: string }[] = [];
|
||||
for (const header of finalCredentials.headers) {
|
||||
if (header.value === "******") {
|
||||
const oldValue = oldHeadersMap[header.key];
|
||||
if (oldValue) {
|
||||
finalHeaders.push({ key: header.key, value: oldValue });
|
||||
}
|
||||
: {})
|
||||
} else {
|
||||
finalHeaders.push(header);
|
||||
}
|
||||
}
|
||||
finalCredentials.headers = finalHeaders;
|
||||
}
|
||||
|
||||
const factory = LOG_STREAM_FACTORY_MAP[provider]();
|
||||
const validatedCredentials = await factory.validateCredentials({ credentials: finalCredentials });
|
||||
|
||||
const encryptedCredentials = await encryptLogStreamCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
return updatedLogStream;
|
||||
|
||||
const updatedLogStream = await auditLogStreamDAL.updateById(logStreamId, {
|
||||
encryptedCredentials
|
||||
});
|
||||
|
||||
return { ...updatedLogStream, credentials: validatedCredentials } as TAuditLogStream;
|
||||
};
|
||||
|
||||
const deleteById: TAuditLogStreamServiceFactory["deleteById"] = async ({
|
||||
id,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
const deleteById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => {
|
||||
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` });
|
||||
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
logStream.orgId
|
||||
);
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
const deletedLogStream = await auditLogStreamDAL.deleteById(id);
|
||||
return deletedLogStream;
|
||||
if (logStream.provider !== provider) {
|
||||
throw new BadRequestError({
|
||||
message: `Audit Log Stream with ID '${logStreamId}' is not for provider '${provider}'`
|
||||
});
|
||||
}
|
||||
|
||||
const deletedLogStream = await auditLogStreamDAL.deleteById(logStreamId);
|
||||
|
||||
return decryptLogStream(deletedLogStream, kmsService);
|
||||
};
|
||||
|
||||
const getById: TAuditLogStreamServiceFactory["getById"] = async ({
|
||||
id,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}) => {
|
||||
const logStream = await auditLogStreamDAL.findById(id);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
||||
const getById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => {
|
||||
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` });
|
||||
|
||||
const headers =
|
||||
logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.decryptWithRootEncryptionKey({
|
||||
tag: logStream.encryptedHeadersTag,
|
||||
iv: logStream.encryptedHeadersIV,
|
||||
ciphertext: logStream.encryptedHeadersCiphertext,
|
||||
keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: undefined;
|
||||
|
||||
return { ...logStream, headers };
|
||||
};
|
||||
|
||||
const list: TAuditLogStreamServiceFactory["list"] = async ({ actor, actorId, actorOrgId, actorAuthMethod }) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actor.type,
|
||||
actor.id,
|
||||
logStream.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
return logStreams;
|
||||
if (logStream.provider !== provider) {
|
||||
throw new BadRequestError({
|
||||
message: `Audit Log Stream with ID '${logStreamId}' is not for provider '${provider}'`
|
||||
});
|
||||
}
|
||||
|
||||
return decryptLogStream(logStream, kmsService);
|
||||
};
|
||||
|
||||
const list = async (actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const logStreams = await auditLogStreamDAL.find({ orgId: actor.orgId });
|
||||
|
||||
return Promise.all(logStreams.map((stream) => decryptLogStream(stream, kmsService)));
|
||||
};
|
||||
|
||||
const streamLog = async (orgId: string, auditLog: TAuditLogs) => {
|
||||
const logStreams = await auditLogStreamDAL.find({ orgId });
|
||||
await Promise.allSettled(
|
||||
logStreams.map(async ({ provider, encryptedCredentials }) => {
|
||||
const credentials = await decryptLogStreamCredentials({
|
||||
encryptedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const factory = LOG_STREAM_FACTORY_MAP[provider as LogProvider]();
|
||||
|
||||
try {
|
||||
await factory.streamLog({
|
||||
credentials,
|
||||
auditLog
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`Failed to stream audit log [auditLogId=${auditLog.id}] [provider=${provider}] [orgId=${orgId}]${error instanceof AxiosError ? `: ${error.message}` : ""}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -244,6 +254,8 @@ export const auditLogStreamServiceFactory = ({
|
||||
updateById,
|
||||
deleteById,
|
||||
getById,
|
||||
list
|
||||
list,
|
||||
listProviderOptions,
|
||||
streamLog
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,48 +1,42 @@
|
||||
import { TAuditLogStreams } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { TAuditLogs } from "@app/db/schemas";
|
||||
|
||||
export type LogStreamHeaders = {
|
||||
key: string;
|
||||
value: string;
|
||||
import { LogProvider } from "./audit-log-stream-enums";
|
||||
import { TAzureProvider, TAzureProviderCredentials } from "./azure/azure-provider-types";
|
||||
import { TCriblProvider, TCriblProviderCredentials } from "./cribl/cribl-provider-types";
|
||||
import { TCustomProvider, TCustomProviderCredentials } from "./custom/custom-provider-types";
|
||||
import { TDatadogProvider, TDatadogProviderCredentials } from "./datadog/datadog-provider-types";
|
||||
import { TSplunkProvider, TSplunkProviderCredentials } from "./splunk/splunk-provider-types";
|
||||
|
||||
export type TAuditLogStream = TDatadogProvider | TSplunkProvider | TCustomProvider | TAzureProvider | TCriblProvider;
|
||||
|
||||
export type TAuditLogStreamCredentials =
|
||||
| TDatadogProviderCredentials
|
||||
| TSplunkProviderCredentials
|
||||
| TCustomProviderCredentials
|
||||
| TAzureProviderCredentials
|
||||
| TCriblProviderCredentials;
|
||||
|
||||
export type TCreateAuditLogStreamDTO = {
|
||||
provider: LogProvider;
|
||||
credentials: TAuditLogStreamCredentials;
|
||||
};
|
||||
|
||||
export type TCreateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
url: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
export type TUpdateAuditLogStreamDTO = {
|
||||
logStreamId: string;
|
||||
provider: LogProvider;
|
||||
credentials: TAuditLogStreamCredentials;
|
||||
};
|
||||
|
||||
export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
url?: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
};
|
||||
export type TLogStreamFactoryValidateCredentials<C extends TAuditLogStreamCredentials> = (input: {
|
||||
credentials: C;
|
||||
}) => Promise<C>;
|
||||
|
||||
export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
};
|
||||
export type TLogStreamFactoryStreamLog<C extends TAuditLogStreamCredentials> = (input: {
|
||||
credentials: C;
|
||||
auditLog: TAuditLogs;
|
||||
}) => Promise<void>;
|
||||
|
||||
export type TListAuditLogStreamDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TAuditLogStreamServiceFactory = {
|
||||
create: (arg: TCreateAuditLogStreamDTO) => Promise<TAuditLogStreams>;
|
||||
updateById: (arg: TUpdateAuditLogStreamDTO) => Promise<TAuditLogStreams>;
|
||||
deleteById: (arg: TDeleteAuditLogStreamDTO) => Promise<TAuditLogStreams>;
|
||||
getById: (arg: TGetDetailsAuditLogStreamDTO) => Promise<{
|
||||
headers: LogStreamHeaders[] | undefined;
|
||||
orgId: string;
|
||||
url: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
encryptedHeadersCiphertext?: string | null | undefined;
|
||||
encryptedHeadersIV?: string | null | undefined;
|
||||
encryptedHeadersTag?: string | null | undefined;
|
||||
encryptedHeadersAlgorithm?: string | null | undefined;
|
||||
encryptedHeadersKeyEncoding?: string | null | undefined;
|
||||
}>;
|
||||
list: (arg: TListAuditLogStreamDTO) => Promise<TAuditLogStreams[]>;
|
||||
export type TLogStreamFactory<C extends TAuditLogStreamCredentials> = () => {
|
||||
validateCredentials: TLogStreamFactoryValidateCredentials<C>;
|
||||
streamLog: TLogStreamFactoryStreamLog<C>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue";
|
||||
import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types";
|
||||
import { TAzureProviderCredentials } from "./azure-provider-types";
|
||||
|
||||
function createPayload(event: { createdAt?: Date | string } & Record<string, unknown>) {
|
||||
return [
|
||||
{
|
||||
...event,
|
||||
TimeGenerated: (event.createdAt ? new Date(event.createdAt) : new Date()).toISOString()
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async function getAzureToken(tenantId: string, clientId: string, clientSecret: string) {
|
||||
const { data } = await request.post<{ access_token: string }>(
|
||||
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||
new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://monitor.azure.com/.default"
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
export const AzureProviderFactory = () => {
|
||||
const validateCredentials: TLogStreamFactoryValidateCredentials<TAzureProviderCredentials> = async ({
|
||||
credentials
|
||||
}) => {
|
||||
const { tenantId, clientId, clientSecret, dceUrl, dcrId, cltName } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(dceUrl);
|
||||
|
||||
const token = await getAzureToken(tenantId, clientId, clientSecret);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
|
||||
await request
|
||||
.post(
|
||||
`${dceUrl}/dataCollectionRules/${dcrId}/streams/Custom-${cltName}_CL?api-version=2023-01-01`,
|
||||
createPayload({ ping: "ok" }),
|
||||
{
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new BadRequestError({ message: `Failed to connect with Azure: ${(err as Error)?.message}` });
|
||||
});
|
||||
|
||||
return credentials;
|
||||
};
|
||||
|
||||
const streamLog: TLogStreamFactoryStreamLog<TAzureProviderCredentials> = async ({ credentials, auditLog }) => {
|
||||
const { tenantId, clientId, clientSecret, dceUrl, dcrId, cltName } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(dceUrl);
|
||||
|
||||
const token = await getAzureToken(tenantId, clientId, clientSecret);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
|
||||
await request.post(
|
||||
`${dceUrl}/dataCollectionRules/${dcrId}/streams/Custom-${cltName}_CL?api-version=2023-01-01`,
|
||||
createPayload(auditLog),
|
||||
{
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
validateCredentials,
|
||||
streamLog
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
|
||||
export const getAzureProviderListItem = () => {
|
||||
return {
|
||||
name: "Azure" as const,
|
||||
provider: LogProvider.Azure as const
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const AzureProviderCredentialsSchema = z.object({
|
||||
tenantId: z.string().trim().uuid(),
|
||||
clientId: z.string().trim().uuid(),
|
||||
clientSecret: z.string().trim().length(40),
|
||||
|
||||
// Data Collection Endpoint URL
|
||||
dceUrl: z.string().trim().url().min(1).max(255),
|
||||
|
||||
// Data Collection Rule Immutable ID
|
||||
dcrId: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => new RE2(/^dcr-[0-9a-f]{32}$/).test(val), "DCR ID must be in dcr-*** format"),
|
||||
|
||||
// Custom Log Table Name
|
||||
cltName: z.string().trim().min(1).max(255)
|
||||
});
|
||||
|
||||
const BaseAzureProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Azure) });
|
||||
|
||||
export const AzureProviderSchema = BaseAzureProviderSchema.extend({
|
||||
credentials: AzureProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedAzureProviderSchema = BaseAzureProviderSchema.extend({
|
||||
credentials: AzureProviderCredentialsSchema.pick({
|
||||
tenantId: true,
|
||||
clientId: true,
|
||||
dceUrl: true,
|
||||
dcrId: true,
|
||||
cltName: true
|
||||
})
|
||||
});
|
||||
|
||||
export const AzureProviderListItemSchema = z.object({
|
||||
name: z.literal("Azure"),
|
||||
provider: z.literal(LogProvider.Azure)
|
||||
});
|
||||
|
||||
export const CreateAzureProviderLogStreamSchema = z.object({
|
||||
credentials: AzureProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateAzureProviderLogStreamSchema = z.object({
|
||||
credentials: AzureProviderCredentialsSchema
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AzureProviderCredentialsSchema, AzureProviderSchema } from "./azure-provider-schemas";
|
||||
|
||||
export type TAzureProvider = z.infer<typeof AzureProviderSchema>;
|
||||
|
||||
export type TAzureProviderCredentials = z.infer<typeof AzureProviderCredentialsSchema>;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue";
|
||||
import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types";
|
||||
import { TCriblProviderCredentials } from "./cribl-provider-types";
|
||||
|
||||
export const CriblProviderFactory = () => {
|
||||
const validateCredentials: TLogStreamFactoryValidateCredentials<TCriblProviderCredentials> = async ({
|
||||
credentials
|
||||
}) => {
|
||||
const { url, token } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
|
||||
await request
|
||||
.post(url, JSON.stringify({ ping: "ok" }), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new BadRequestError({ message: `Failed to connect with Cribl: ${(err as Error)?.message}` });
|
||||
});
|
||||
|
||||
return credentials;
|
||||
};
|
||||
|
||||
const streamLog: TLogStreamFactoryStreamLog<TCriblProviderCredentials> = async ({ credentials, auditLog }) => {
|
||||
const { url, token } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
|
||||
await request.post(url, JSON.stringify(auditLog), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
validateCredentials,
|
||||
streamLog
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
|
||||
export const getCriblProviderListItem = () => {
|
||||
return {
|
||||
name: "Cribl" as const,
|
||||
provider: LogProvider.Cribl as const
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const CriblProviderCredentialsSchema = z.object({
|
||||
url: z.string().url().trim().min(1).max(255),
|
||||
token: z.string().trim().min(21).max(255)
|
||||
});
|
||||
|
||||
const BaseCriblProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Cribl) });
|
||||
|
||||
export const CriblProviderSchema = BaseCriblProviderSchema.extend({
|
||||
credentials: CriblProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedCriblProviderSchema = BaseCriblProviderSchema.extend({
|
||||
credentials: CriblProviderCredentialsSchema.pick({
|
||||
url: true
|
||||
})
|
||||
});
|
||||
|
||||
export const CriblProviderListItemSchema = z.object({
|
||||
name: z.literal("Cribl"),
|
||||
provider: z.literal(LogProvider.Cribl)
|
||||
});
|
||||
|
||||
export const CreateCriblProviderLogStreamSchema = z.object({
|
||||
credentials: CriblProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateCriblProviderLogStreamSchema = z.object({
|
||||
credentials: CriblProviderCredentialsSchema
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CriblProviderCredentialsSchema, CriblProviderSchema } from "./cribl-provider-schemas";
|
||||
|
||||
export type TCriblProvider = z.infer<typeof CriblProviderSchema>;
|
||||
|
||||
export type TCriblProviderCredentials = z.infer<typeof CriblProviderCredentialsSchema>;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue";
|
||||
import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types";
|
||||
import { TCustomProviderCredentials } from "./custom-provider-types";
|
||||
|
||||
export const CustomProviderFactory = () => {
|
||||
const validateCredentials: TLogStreamFactoryValidateCredentials<TCustomProviderCredentials> = async ({
|
||||
credentials
|
||||
}) => {
|
||||
const { url, headers } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
if (headers.length) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
await request
|
||||
.post(
|
||||
url,
|
||||
{ ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` });
|
||||
});
|
||||
|
||||
return credentials;
|
||||
};
|
||||
|
||||
const streamLog: TLogStreamFactoryStreamLog<TCustomProviderCredentials> = async ({ credentials, auditLog }) => {
|
||||
const { url, headers } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
if (headers.length) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
streamHeaders[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
await request.post(url, auditLog, {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
validateCredentials,
|
||||
streamLog
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
|
||||
export const getCustomProviderListItem = () => {
|
||||
return {
|
||||
name: "Custom" as const,
|
||||
provider: LogProvider.Custom as const
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const CustomProviderCredentialsSchema = z.object({
|
||||
url: z.string().url().trim().min(1).max(255),
|
||||
headers: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((val) => new RE2(/^[^\n\r]+$/).test(val), "Header keys cannot contain newlines or carriage returns"),
|
||||
value: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((val) => new RE2(/^[^\n\r]+$/).test(val), "Header values cannot contain newlines or carriage returns")
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
const BaseCustomProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Custom) });
|
||||
|
||||
export const CustomProviderSchema = BaseCustomProviderSchema.extend({
|
||||
credentials: CustomProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedCustomProviderSchema = BaseCustomProviderSchema.extend({
|
||||
credentials: z.object({
|
||||
url: CustomProviderCredentialsSchema.shape.url,
|
||||
// Return header keys and a redacted value
|
||||
headers: CustomProviderCredentialsSchema.shape.headers.transform((headers) =>
|
||||
headers.map((header) => ({ ...header, value: "******" }))
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
export const CustomProviderListItemSchema = z.object({
|
||||
name: z.literal("Custom"),
|
||||
provider: z.literal(LogProvider.Custom)
|
||||
});
|
||||
|
||||
export const CreateCustomProviderLogStreamSchema = z.object({
|
||||
credentials: CustomProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateCustomProviderLogStreamSchema = z.object({
|
||||
credentials: CustomProviderCredentialsSchema
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CustomProviderCredentialsSchema, CustomProviderSchema } from "./custom-provider-schemas";
|
||||
|
||||
export type TCustomProvider = z.infer<typeof CustomProviderSchema>;
|
||||
|
||||
export type TCustomProviderCredentials = z.infer<typeof CustomProviderCredentialsSchema>;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue";
|
||||
import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types";
|
||||
import { TDatadogProviderCredentials } from "./datadog-provider-types";
|
||||
|
||||
function createPayload(event: Record<string, unknown>) {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const ddtags = [`env:${appCfg.NODE_ENV || "unknown"}`].join(",");
|
||||
|
||||
return {
|
||||
...event,
|
||||
hostname: new URL(appCfg.SITE_URL || "http://infisical").hostname,
|
||||
ddsource: "infisical",
|
||||
service: "infisical",
|
||||
ddtags
|
||||
};
|
||||
}
|
||||
|
||||
export const DatadogProviderFactory = () => {
|
||||
const validateCredentials: TLogStreamFactoryValidateCredentials<TDatadogProviderCredentials> = async ({
|
||||
credentials
|
||||
}) => {
|
||||
const { url, token } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token };
|
||||
|
||||
await request
|
||||
.post(url, createPayload({ ping: "ok" }), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new BadRequestError({ message: `Failed to connect with Datadog: ${(err as Error)?.message}` });
|
||||
});
|
||||
|
||||
return credentials;
|
||||
};
|
||||
|
||||
const streamLog: TLogStreamFactoryStreamLog<TDatadogProviderCredentials> = async ({ credentials, auditLog }) => {
|
||||
const { url, token } = credentials;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token };
|
||||
|
||||
await request.post(url, createPayload(auditLog), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
validateCredentials,
|
||||
streamLog
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
|
||||
export const getDatadogProviderListItem = () => {
|
||||
return {
|
||||
name: "Datadog" as const,
|
||||
provider: LogProvider.Datadog as const
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const DatadogProviderCredentialsSchema = z.object({
|
||||
url: z.string().url().trim().min(1).max(255),
|
||||
token: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => new RE2(/^[a-fA-F0-9]{32}$/).test(val), "Invalid Datadog API key format")
|
||||
});
|
||||
|
||||
const BaseDatadogProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Datadog) });
|
||||
|
||||
export const DatadogProviderSchema = BaseDatadogProviderSchema.extend({
|
||||
credentials: DatadogProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedDatadogProviderSchema = BaseDatadogProviderSchema.extend({
|
||||
credentials: DatadogProviderCredentialsSchema.pick({
|
||||
url: true
|
||||
})
|
||||
});
|
||||
|
||||
export const DatadogProviderListItemSchema = z.object({
|
||||
name: z.literal("Datadog"),
|
||||
provider: z.literal(LogProvider.Datadog)
|
||||
});
|
||||
|
||||
export const CreateDatadogProviderLogStreamSchema = z.object({
|
||||
credentials: DatadogProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateDatadogProviderLogStreamSchema = z.object({
|
||||
credentials: DatadogProviderCredentialsSchema
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DatadogProviderCredentialsSchema, DatadogProviderSchema } from "./datadog-provider-schemas";
|
||||
|
||||
export type TDatadogProvider = z.infer<typeof DatadogProviderSchema>;
|
||||
|
||||
export type TDatadogProviderCredentials = z.infer<typeof DatadogProviderCredentialsSchema>;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
|
||||
import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue";
|
||||
import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types";
|
||||
import { TSplunkProviderCredentials } from "./splunk-provider-types";
|
||||
|
||||
function createPayload(event: Record<string, unknown>) {
|
||||
const appCfg = getConfig();
|
||||
|
||||
return {
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
...(appCfg.SITE_URL && { host: new URL(appCfg.SITE_URL).host }),
|
||||
source: "infisical",
|
||||
sourcetype: "_json",
|
||||
event
|
||||
};
|
||||
}
|
||||
|
||||
async function createSplunkUrl(hostname: string) {
|
||||
let parsedHostname: string;
|
||||
try {
|
||||
parsedHostname = new URL(`https://${hostname}`).hostname;
|
||||
} catch (error) {
|
||||
throw new BadRequestError({ message: `Invalid Splunk hostname provided: ${(error as Error).message}` });
|
||||
}
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(`https://${parsedHostname}`);
|
||||
|
||||
return `https://${parsedHostname}:8088/services/collector/event`;
|
||||
}
|
||||
|
||||
export const SplunkProviderFactory = () => {
|
||||
const validateCredentials: TLogStreamFactoryValidateCredentials<TSplunkProviderCredentials> = async ({
|
||||
credentials
|
||||
}) => {
|
||||
const { hostname, token } = credentials;
|
||||
|
||||
const url = await createSplunkUrl(hostname);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Splunk ${token}`
|
||||
};
|
||||
|
||||
await request
|
||||
.post(url, createPayload({ ping: "ok" }), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new BadRequestError({ message: `Failed to connect with Splunk: ${(err as Error)?.message}` });
|
||||
});
|
||||
|
||||
return credentials;
|
||||
};
|
||||
|
||||
const streamLog: TLogStreamFactoryStreamLog<TSplunkProviderCredentials> = async ({ credentials, auditLog }) => {
|
||||
const { hostname, token } = credentials;
|
||||
|
||||
const url = await createSplunkUrl(hostname);
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Splunk ${token}`
|
||||
};
|
||||
|
||||
await request.post(url, createPayload(auditLog), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
validateCredentials,
|
||||
streamLog
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
|
||||
export const getSplunkProviderListItem = () => {
|
||||
return {
|
||||
name: "Splunk" as const,
|
||||
provider: LogProvider.Splunk as const
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const SplunkProviderCredentialsSchema = z.object({
|
||||
hostname: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.superRefine((val, ctx) => {
|
||||
if (val.includes("://")) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Hostname should not include protocol"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(`https://${val}`);
|
||||
if (url.hostname !== val) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Must be a valid hostname without port or path"
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid hostname" });
|
||||
}
|
||||
}),
|
||||
token: z.string().uuid().trim().min(1)
|
||||
});
|
||||
|
||||
const BaseSplunkProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Splunk) });
|
||||
|
||||
export const SplunkProviderSchema = BaseSplunkProviderSchema.extend({
|
||||
credentials: SplunkProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedSplunkProviderSchema = BaseSplunkProviderSchema.extend({
|
||||
credentials: SplunkProviderCredentialsSchema.pick({
|
||||
hostname: true
|
||||
})
|
||||
});
|
||||
|
||||
export const SplunkProviderListItemSchema = z.object({
|
||||
name: z.literal("Splunk"),
|
||||
provider: z.literal(LogProvider.Splunk)
|
||||
});
|
||||
|
||||
export const CreateSplunkProviderLogStreamSchema = z.object({
|
||||
credentials: SplunkProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateSplunkProviderLogStreamSchema = z.object({
|
||||
credentials: SplunkProviderCredentialsSchema
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SplunkProviderCredentialsSchema, SplunkProviderSchema } from "./splunk-provider-schemas";
|
||||
|
||||
export type TSplunkProvider = z.infer<typeof SplunkProviderSchema>;
|
||||
|
||||
export type TSplunkProviderCredentials = z.infer<typeof SplunkProviderCredentialsSchema>;
|
||||
@@ -1,22 +1,14 @@
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal";
|
||||
import { providerSpecificPayload } from "../audit-log-stream/audit-log-stream-fns";
|
||||
import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||
import { TCreateAuditLogDTO } from "./audit-log-types";
|
||||
|
||||
type TAuditLogQueueServiceFactoryDep = {
|
||||
auditLogDAL: TAuditLogDALFactory;
|
||||
auditLogStreamDAL: Pick<TAuditLogStreamDALFactory, "find">;
|
||||
auditLogStreamService: Pick<TAuditLogStreamServiceFactory, "streamLog">;
|
||||
queueService: TQueueServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -35,7 +27,7 @@ export const auditLogQueueServiceFactory = async ({
|
||||
queueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
auditLogStreamDAL
|
||||
auditLogStreamService
|
||||
}: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
|
||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
@@ -86,60 +78,7 @@ export const auditLogQueueServiceFactory = async ({
|
||||
userAgentType
|
||||
});
|
||||
|
||||
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
||||
await Promise.allSettled(
|
||||
logStreams.map(
|
||||
async ({
|
||||
url,
|
||||
encryptedHeadersTag,
|
||||
encryptedHeadersIV,
|
||||
encryptedHeadersKeyEncoding,
|
||||
encryptedHeadersCiphertext
|
||||
}) => {
|
||||
const streamHeaders =
|
||||
encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
|
||||
? (JSON.parse(
|
||||
crypto
|
||||
.encryption()
|
||||
.symmetric()
|
||||
.decryptWithRootEncryptionKey({
|
||||
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
|
||||
iv: encryptedHeadersIV,
|
||||
tag: encryptedHeadersTag,
|
||||
ciphertext: encryptedHeadersCiphertext
|
||||
})
|
||||
) as LogStreamHeaders[])
|
||||
: [];
|
||||
|
||||
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
||||
if (streamHeaders.length)
|
||||
streamHeaders.forEach(({ key, value }) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await request.post(
|
||||
url,
|
||||
{ ...providerSpecificPayload(url), ...auditLog },
|
||||
{
|
||||
headers,
|
||||
// request timeout
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
// connection timeout
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
|
||||
);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
await auditLogStreamService.streamLog(orgId, auditLog);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export enum EventType {
|
||||
MOVE_SECRETS = "move-secrets",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
GET_PROJECT_KEY = "get-project-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
@@ -199,6 +199,7 @@ export enum EventType {
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
|
||||
CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS = "clear-identity-ldap-auth-lockouts",
|
||||
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
||||
@@ -249,9 +250,9 @@ export enum EventType {
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
GET_ENVIRONMENT = "get-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
ADD_PROJECT_MEMBER = "add-project-member",
|
||||
ADD_BATCH_PROJECT_MEMBER = "add-project-members",
|
||||
REMOVE_PROJECT_MEMBER = "remove-project-member",
|
||||
CREATE_FOLDER = "create-folder",
|
||||
UPDATE_FOLDER = "update-folder",
|
||||
DELETE_FOLDER = "delete-folder",
|
||||
@@ -264,8 +265,8 @@ export enum EventType {
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
|
||||
UPDATE_USER_PROJECT_ROLE = "update-user-project-role",
|
||||
UPDATE_USER_PROJECT_DENIED_PERMISSIONS = "update-user-project-denied-permissions",
|
||||
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||
@@ -392,6 +393,8 @@ export enum EventType {
|
||||
CREATE_APP_CONNECTION = "create-app-connection",
|
||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||
DELETE_APP_CONNECTION = "delete-app-connection",
|
||||
GET_APP_CONNECTION_USAGE = "get-app-connection-usage",
|
||||
MIGRATE_APP_CONNECTION = "migrate-app-connection",
|
||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||
CREATE_SECRET_REQUEST = "create-secret-request",
|
||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||
@@ -664,8 +667,8 @@ interface DeleteSecretBatchEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetWorkspaceKeyEvent {
|
||||
type: EventType.GET_WORKSPACE_KEY;
|
||||
interface GetProjectKeyEvent {
|
||||
type: EventType.GET_PROJECT_KEY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
@@ -1370,6 +1373,10 @@ interface AddIdentityLdapAuthEvent {
|
||||
allowedFields?: TAllowedFields[];
|
||||
url: string;
|
||||
templateId?: string | null;
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDurationSeconds: number;
|
||||
lockoutCounterResetSeconds: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1384,6 +1391,10 @@ interface UpdateIdentityLdapAuthEvent {
|
||||
allowedFields?: TAllowedFields[];
|
||||
url?: string;
|
||||
templateId?: string | null;
|
||||
lockoutEnabled?: boolean;
|
||||
lockoutThreshold?: number;
|
||||
lockoutDurationSeconds?: number;
|
||||
lockoutCounterResetSeconds?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1401,6 +1412,13 @@ interface RevokeIdentityLdapAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface ClearIdentityLdapAuthLockoutsEvent {
|
||||
type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityOidcAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
@@ -1557,24 +1575,24 @@ interface DeleteEnvironmentEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface AddWorkspaceMemberEvent {
|
||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
||||
interface AddProjectMemberEvent {
|
||||
type: EventType.ADD_PROJECT_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddBatchWorkspaceMemberEvent {
|
||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER;
|
||||
interface AddBatchProjectMemberEvent {
|
||||
type: EventType.ADD_BATCH_PROJECT_MEMBER;
|
||||
metadata: Array<{
|
||||
userId: string;
|
||||
email: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RemoveWorkspaceMemberEvent {
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
||||
interface RemoveProjectMemberEvent {
|
||||
type: EventType.REMOVE_PROJECT_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
@@ -1713,7 +1731,7 @@ interface DeleteSecretImportEvent {
|
||||
}
|
||||
|
||||
interface UpdateUserRole {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
|
||||
type: EventType.UPDATE_USER_PROJECT_ROLE;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
@@ -1723,7 +1741,7 @@ interface UpdateUserRole {
|
||||
}
|
||||
|
||||
interface UpdateUserDeniedPermissions {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
|
||||
type: EventType.UPDATE_USER_PROJECT_DENIED_PERMISSIONS;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
@@ -2781,14 +2799,31 @@ interface GetAppConnectionEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAppConnectionUsageEvent {
|
||||
type: EventType.GET_APP_CONNECTION_USAGE;
|
||||
metadata: {
|
||||
connectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MigrateAppConnectionEvent {
|
||||
type: EventType.MIGRATE_APP_CONNECTION;
|
||||
metadata: {
|
||||
connectionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateAppConnectionEvent {
|
||||
type: EventType.CREATE_APP_CONNECTION;
|
||||
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
|
||||
metadata: Omit<TCreateAppConnectionDTO, "credentials" | "projectId"> & { connectionId: string };
|
||||
}
|
||||
|
||||
interface UpdateAppConnectionEvent {
|
||||
type: EventType.UPDATE_APP_CONNECTION;
|
||||
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
|
||||
metadata: Omit<TUpdateAppConnectionDTO, "credentials" | "projectId"> & {
|
||||
connectionId: string;
|
||||
credentialsUpdated: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteAppConnectionEvent {
|
||||
@@ -3477,7 +3512,7 @@ export type Event =
|
||||
| MoveSecretsEvent
|
||||
| DeleteSecretEvent
|
||||
| DeleteSecretBatchEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| GetProjectKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UpdateIntegrationAuthEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
@@ -3562,13 +3597,14 @@ export type Event =
|
||||
| UpdateIdentityLdapAuthEvent
|
||||
| GetIdentityLdapAuthEvent
|
||||
| RevokeIdentityLdapAuthEvent
|
||||
| ClearIdentityLdapAuthLockoutsEvent
|
||||
| CreateEnvironmentEvent
|
||||
| GetEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
| AddBatchWorkspaceMemberEvent
|
||||
| RemoveWorkspaceMemberEvent
|
||||
| AddProjectMemberEvent
|
||||
| AddBatchProjectMemberEvent
|
||||
| RemoveProjectMemberEvent
|
||||
| CreateFolderEvent
|
||||
| UpdateFolderEvent
|
||||
| DeleteFolderEvent
|
||||
@@ -3697,6 +3733,8 @@ export type Event =
|
||||
| CreateAppConnectionEvent
|
||||
| UpdateAppConnectionEvent
|
||||
| DeleteAppConnectionEvent
|
||||
| GetAppConnectionUsageEvent
|
||||
| MigrateAppConnectionEvent
|
||||
| GetSshHostGroupEvent
|
||||
| CreateSshHostGroupEvent
|
||||
| UpdateSshHostGroupEvent
|
||||
|
||||
@@ -46,7 +46,10 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
|
||||
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
|
||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
||||
.count("*")
|
||||
.where({ dynamicSecretId })
|
||||
.first();
|
||||
return parseInt(doc || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
||||
@@ -55,7 +58,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease)
|
||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
||||
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
||||
.first()
|
||||
.join(
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||
import { TGatewayDALFactory } from "../gateway/gateway-dal";
|
||||
import { TGatewayV2DALFactory } from "../gateway-v2/gateway-v2-dal";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
|
||||
@@ -39,6 +40,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
|
||||
gatewayV2DAL: Pick<TGatewayV2DALFactory, "findOne" | "find">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
};
|
||||
|
||||
@@ -53,6 +55,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectDAL,
|
||||
kmsService,
|
||||
gatewayDAL,
|
||||
gatewayV2DAL,
|
||||
resourceMetadataDAL
|
||||
}: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
|
||||
const create: TDynamicSecretServiceFactory["create"] = async ({
|
||||
@@ -70,6 +73,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
metadata,
|
||||
usernameTemplate
|
||||
}) => {
|
||||
let isGatewayV1 = true;
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
@@ -118,17 +122,22 @@ export const dynamicSecretServiceFactory = ({
|
||||
const gatewayId = inputs.gatewayId as string;
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
|
||||
const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId });
|
||||
|
||||
if (!gateway) {
|
||||
if (!gateway && !gatewayv2) {
|
||||
throw new NotFoundError({
|
||||
message: `Gateway with ID ${gatewayId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (!gateway) {
|
||||
isGatewayV1 = false;
|
||||
}
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
gateway.orgId,
|
||||
gateway?.orgId ?? gatewayv2?.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
@@ -138,7 +147,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
selectedGatewayId = gateway.id;
|
||||
selectedGatewayId = gateway?.id ?? gatewayv2?.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId });
|
||||
@@ -159,7 +168,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
gatewayId: selectedGatewayId,
|
||||
gatewayId: isGatewayV1 ? selectedGatewayId : undefined,
|
||||
gatewayV2Id: isGatewayV1 ? undefined : selectedGatewayId,
|
||||
usernameTemplate
|
||||
},
|
||||
tx
|
||||
@@ -180,7 +190,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
return cfg;
|
||||
});
|
||||
|
||||
return dynamicSecretCfg;
|
||||
return { ...dynamicSecretCfg, inputs };
|
||||
};
|
||||
|
||||
const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
|
||||
@@ -270,20 +280,27 @@ export const dynamicSecretServiceFactory = ({
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
let isGatewayV1 = true;
|
||||
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
|
||||
const gatewayId = updatedInput.gatewayId as string;
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
|
||||
if (!gateway) {
|
||||
const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId });
|
||||
|
||||
if (!gateway && !gatewayv2) {
|
||||
throw new NotFoundError({
|
||||
message: `Gateway with ID ${gatewayId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (!gateway) {
|
||||
isGatewayV1 = false;
|
||||
}
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
gateway.orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
@@ -293,7 +310,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
selectedGatewayId = gateway.id;
|
||||
selectedGatewayId = gateway?.id ?? gatewayv2?.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput, { projectId });
|
||||
@@ -309,7 +326,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
gatewayId: selectedGatewayId,
|
||||
gatewayId: isGatewayV1 ? selectedGatewayId : null,
|
||||
gatewayV2Id: isGatewayV1 ? null : selectedGatewayId,
|
||||
usernameTemplate
|
||||
},
|
||||
tx
|
||||
@@ -337,7 +355,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
return cfg;
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
return { ...updatedDynamicCfg, inputs: updatedInput };
|
||||
};
|
||||
|
||||
const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
|
||||
|
||||
@@ -30,7 +30,7 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s
|
||||
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
||||
const hostIps = await Promise.all(
|
||||
await Promise.all(
|
||||
providerInputs.host
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
@@ -48,10 +48,10 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
|
||||
return { ...providerInputs, hostIps };
|
||||
return { ...providerInputs };
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema> & { hostIps: string[] }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const client = new cassandra.Client({
|
||||
sslOptions,
|
||||
@@ -64,7 +64,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
},
|
||||
keyspace: providerInputs.keyspace,
|
||||
localDataCenter: providerInputs?.localDataCenter,
|
||||
contactPoints: providerInputs.hostIps
|
||||
contactPoints: providerInputs.host.split(",")
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
@@ -28,14 +28,14 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s
|
||||
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs, hostIp };
|
||||
await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs };
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||
const connection = new ElasticSearchClient({
|
||||
node: {
|
||||
url: new URL(`${providerInputs.hostIp}:${providerInputs.port}`),
|
||||
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
||||
...(providerInputs.ca && {
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
@@ -24,12 +25,14 @@ import { VerticaProvider } from "./vertica";
|
||||
|
||||
type TBuildDynamicSecretProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
};
|
||||
|
||||
export const buildDynamicSecretProviders = ({
|
||||
gatewayService
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService, gatewayV2Service }),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||
@@ -44,7 +47,7 @@ export const buildDynamicSecretProviders = ({
|
||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService, gatewayV2Service }),
|
||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
|
||||
[DynamicSecretProviders.Github]: GithubProvider(),
|
||||
|
||||
@@ -5,12 +5,14 @@ import https from "https";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||
|
||||
import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||
import {
|
||||
DynamicSecretKubernetesSchema,
|
||||
KubernetesAuthMethod,
|
||||
@@ -26,6 +28,7 @@ const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local";
|
||||
|
||||
type TKubernetesProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
@@ -38,7 +41,10 @@ const generateUsername = (usernameTemplate?: string | null) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
||||
export const KubernetesProvider = ({
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
}: TKubernetesProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||
if (!providerInputs.gatewayId && providerInputs.url) {
|
||||
@@ -58,6 +64,32 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
},
|
||||
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId: inputs.gatewayId,
|
||||
targetHost: inputs.targetHost,
|
||||
targetPort: inputs.targetPort
|
||||
});
|
||||
if (gatewayV2ConnectionDetails) {
|
||||
const callbackResult = await withGatewayV2Proxy(
|
||||
async (port) => {
|
||||
return gatewayCallback(
|
||||
inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost",
|
||||
port,
|
||||
inputs.httpsAgent
|
||||
);
|
||||
},
|
||||
{
|
||||
relayHost: gatewayV2ConnectionDetails.relayHost,
|
||||
gateway: gatewayV2ConnectionDetails.gateway,
|
||||
relay: gatewayV2ConnectionDetails.relay,
|
||||
protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp,
|
||||
httpsAgent: inputs.httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
return callbackResult;
|
||||
}
|
||||
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
|
||||
@@ -353,8 +385,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
return true;
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response) {
|
||||
let { message } = error?.response?.data as unknown as { message?: string };
|
||||
|
||||
if (!message && typeof error.response.data === "string") {
|
||||
message = error.response.data;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
errorMessage = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
@@ -603,8 +645,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
};
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response) {
|
||||
let { message } = error?.response?.data as unknown as { message?: string };
|
||||
|
||||
if (!message && typeof error.response.data === "string") {
|
||||
message = error.response.data;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
errorMessage = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
@@ -740,8 +792,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
}
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response) {
|
||||
let { message } = error?.response?.data as unknown as { message?: string };
|
||||
|
||||
if (!message && typeof error.response.data === "string") {
|
||||
message = error.response.data;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
errorMessage = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
|
||||
@@ -170,6 +170,7 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().optional(),
|
||||
gatewayId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
@@ -283,11 +284,11 @@ export const DynamicSecretMongoAtlasSchema = z.object({
|
||||
|
||||
export const DynamicSecretMongoDBSchema = z.object({
|
||||
host: z.string().min(1).trim().toLowerCase(),
|
||||
port: z.number().optional(),
|
||||
port: z.number().optional().nullable(),
|
||||
username: z.string().min(1).trim(),
|
||||
password: z.string().min(1).trim(),
|
||||
database: z.string().min(1).trim(),
|
||||
ca: z.string().min(1).optional(),
|
||||
ca: z.string().trim().optional().nullable(),
|
||||
roles: z
|
||||
.string()
|
||||
.array()
|
||||
|
||||
@@ -28,15 +28,15 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s
|
||||
export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs, hostIp };
|
||||
await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs };
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||
const isSrv = !providerInputs.port;
|
||||
const uri = isSrv
|
||||
? `mongodb+srv://${providerInputs.hostIp}`
|
||||
: `mongodb://${providerInputs.hostIp}:${providerInputs.port}`;
|
||||
? `mongodb+srv://${providerInputs.host}`
|
||||
: `mongodb://${providerInputs.host}:${providerInputs.port}`;
|
||||
|
||||
const client = new MongoClient(uri, {
|
||||
auth: {
|
||||
@@ -44,7 +44,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
password: providerInputs.password
|
||||
},
|
||||
directConnection: !isSrv,
|
||||
ca: providerInputs.ca
|
||||
ca: providerInputs.ca || undefined
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
@@ -87,13 +87,13 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa
|
||||
export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs, hostIp };
|
||||
await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs };
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: `${providerInputs.hostIp}:${providerInputs.port}/api`,
|
||||
baseURL: `${providerInputs.host}:${providerInputs.port}/api`,
|
||||
auth: {
|
||||
username: providerInputs.username,
|
||||
password: providerInputs.password
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
await verifyHostInputValidity(providerInputs.host);
|
||||
validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password"].includes(val)
|
||||
});
|
||||
@@ -45,16 +45,13 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
}
|
||||
return { ...providerInputs, hostIp };
|
||||
return { ...providerInputs };
|
||||
};
|
||||
|
||||
const $getClient = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretSapAseSchema> & { hostIp: string },
|
||||
useMaster?: boolean
|
||||
) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
|
||||
const connectionString =
|
||||
`DRIVER={FreeTDS};` +
|
||||
`SERVER=${providerInputs.hostIp};` +
|
||||
`SERVER=${providerInputs.host};` +
|
||||
`PORT=${providerInputs.port};` +
|
||||
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
|
||||
`UID=${providerInputs.username};` +
|
||||
|
||||
@@ -37,7 +37,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
await verifyHostInputValidity(providerInputs.host);
|
||||
validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration"].includes(val)
|
||||
});
|
||||
@@ -49,12 +49,12 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
return { ...providerInputs, hostIp };
|
||||
return { ...providerInputs };
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||
const client = hdb.createClient({
|
||||
host: providerInputs.hostIp,
|
||||
host: providerInputs.host,
|
||||
port: providerInputs.port,
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import handlebars from "handlebars";
|
||||
import knex from "knex";
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
@@ -128,9 +131,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu
|
||||
|
||||
type TSqlDatabaseProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
export const SqlDatabaseProvider = ({
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
}: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
|
||||
@@ -150,17 +157,40 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
return { ...providerInputs, hostIp };
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const $getClient = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema> & { hostIp: string; originalHost: string }
|
||||
) => {
|
||||
const ssl = providerInputs.ca
|
||||
? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host }
|
||||
: undefined;
|
||||
|
||||
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
|
||||
|
||||
/*
|
||||
We route through the gateway by setting connection.host = "localhost".
|
||||
Azure SQL identifies the logical server from the TDS login name when the host
|
||||
isn’t the Azure FQDN. Therefore, when using the gateway, ensure username is
|
||||
"user@<azure-server-name>" so Azure opens the correct logical server.
|
||||
Direct connections to the Azure FQDN usually don’t require this suffix.
|
||||
*/
|
||||
const isAzureSql = isMsSQLClient && new RE2(/\.database\.windows\.net$/i).test(providerInputs.originalHost);
|
||||
const azureServerLabel =
|
||||
isAzureSql && providerInputs.gatewayId ? providerInputs.originalHost?.split(".")[0] : undefined;
|
||||
const effectiveUser =
|
||||
isAzureSql && !providerInputs.username.includes("@") && azureServerLabel
|
||||
? `${providerInputs.username}@${azureServerLabel}`
|
||||
: providerInputs.username;
|
||||
|
||||
const db = knex({
|
||||
client: providerInputs.client,
|
||||
connection: {
|
||||
database: providerInputs.database,
|
||||
port: providerInputs.port,
|
||||
host: providerInputs.host,
|
||||
user: providerInputs.username,
|
||||
host:
|
||||
providerInputs.client === SqlProviders.Postgres && !providerInputs.gatewayId
|
||||
? providerInputs.hostIp
|
||||
: providerInputs.host,
|
||||
user: effectiveUser,
|
||||
password: providerInputs.password,
|
||||
ssl,
|
||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||
@@ -168,6 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||
options: isMsSQLClient
|
||||
? {
|
||||
...(providerInputs.sslEnabled !== undefined ? { encrypt: providerInputs.sslEnabled } : {}),
|
||||
trustServerCertificate: !providerInputs.ca,
|
||||
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
|
||||
}
|
||||
@@ -183,6 +214,26 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||
) => {
|
||||
const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId: providerInputs.gatewayId as string,
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port
|
||||
});
|
||||
|
||||
if (gatewayV2ConnectionDetails) {
|
||||
return withGatewayV2Proxy(
|
||||
async (port) => {
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
relayHost: gatewayV2ConnectionDetails.relayHost,
|
||||
gateway: gatewayV2ConnectionDetails.gateway,
|
||||
relay: gatewayV2ConnectionDetails.relay,
|
||||
protocol: GatewayProxyProtocol.Tcp
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
await withGatewayProxy(
|
||||
@@ -209,8 +260,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
let isConnected = false;
|
||||
const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
hostIp: providerInputs.hostIp,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
@@ -251,7 +308,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
|
||||
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
const db = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
try {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
@@ -294,7 +356,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
const username = entityId;
|
||||
const { database } = providerInputs;
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
const db = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
try {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
@@ -329,7 +396,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
const db = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Redis from "ioredis";
|
||||
import { Cluster, Redis } from "ioredis";
|
||||
import { z } from "zod";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { BusEventSchema, TopicName } from "./types";
|
||||
|
||||
export const eventBusFactory = (redis: Redis) => {
|
||||
export const eventBusFactory = (redis: Redis | Cluster) => {
|
||||
const publisher = redis.duplicate();
|
||||
// Duplicate the publisher to create a subscriber.
|
||||
// This is necessary because Redis does not allow a single connection to both publish and subscribe.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-continue */
|
||||
import { subject } from "@casl/ability";
|
||||
import Redis from "ioredis";
|
||||
import { Cluster, Redis } from "ioredis";
|
||||
|
||||
import { KeyStorePrefixes } from "@app/keystore/keystore";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -12,7 +12,7 @@ import { BusEvent, RegisteredEvent } from "./types";
|
||||
const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
||||
const HEART_BEAT_INTERVAL = 15 * 1000;
|
||||
|
||||
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
|
||||
export const sseServiceFactory = (bus: TEventBusService, redis: Redis | Cluster) => {
|
||||
const clients = new Set<EventStreamClient>();
|
||||
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Readable } from "node:stream";
|
||||
|
||||
import { MongoAbility, PureAbility } from "@casl/ability";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import Redis from "ioredis";
|
||||
import { Cluster, Redis } from "ioredis";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
@@ -65,7 +65,7 @@ export type EventStreamClient = {
|
||||
matcher: PureAbility;
|
||||
};
|
||||
|
||||
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
|
||||
export function createEventStreamClient(redis: Redis | Cluster, options: IEventStreamClientOpts): EventStreamClient {
|
||||
const rules = options.registered.map((r) => {
|
||||
const secretPath = r.conditions?.secretPath;
|
||||
const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const GATEWAY_ROUTING_INFO_OID = "1.3.6.1.4.1.12345.100.1";
|
||||
export const GATEWAY_ACTOR_OID = "1.3.6.1.4.1.12345.100.2";
|
||||
60
backend/src/ee/services/gateway-v2/gateway-v2-dal.ts
Normal file
60
backend/src/ee/services/gateway-v2/gateway-v2-dal.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { GatewaysV2Schema, TableName, TGatewaysV2 } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
|
||||
|
||||
export type TGatewayV2DALFactory = ReturnType<typeof gatewayV2DalFactory>;
|
||||
|
||||
export const gatewayV2DalFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.GatewayV2);
|
||||
|
||||
const find = async (filter: TFindFilter<TGatewaysV2>, { offset, limit, sort, tx }: TFindOpt<TGatewaysV2> = {}) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.GatewayV2)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter, TableName.GatewayV2))
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.GatewayV2}.identityId`)
|
||||
.join(
|
||||
TableName.IdentityOrgMembership,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.GatewayV2}.identityId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.GatewayV2))
|
||||
.select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
|
||||
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
return docs.map((el) => ({
|
||||
...GatewaysV2Schema.parse(el),
|
||||
identity: { id: el.identityId, name: el.identityName }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.GatewayV2}: Find` });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.GatewayV2)
|
||||
.join(TableName.Organization, `${TableName.GatewayV2}.orgId`, `${TableName.Organization}.id`)
|
||||
.where(`${TableName.GatewayV2}.id`, id)
|
||||
.select(selectAllTableCols(TableName.GatewayV2))
|
||||
.select(db.ref("name").withSchema(TableName.Organization).as("orgName"))
|
||||
.first();
|
||||
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.GatewayV2}: Find by id` });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, find, findById };
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user