mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 18:29:07 +00:00
Merge branch 'heads/main' into ENG-3630
This commit is contained in:
@@ -36,11 +36,23 @@ jobs:
|
|||||||
|
|
||||||
echo "Latest production tag: $LATEST_STABLE_TAG"
|
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
|
# Get current date in YYYYMMDD format
|
||||||
DATE=$(date +%Y%m%d)
|
DATE=$(date +%Y%m%d)
|
||||||
|
|
||||||
# Base nightly tag name
|
# Base nightly tag name using next version
|
||||||
BASE_TAG="${LATEST_STABLE_TAG}-nightly-${DATE}"
|
BASE_TAG="${NEXT_VERSION}-nightly-${DATE}"
|
||||||
|
|
||||||
# Check if this exact tag already exists
|
# Check if this exact tag already exists
|
||||||
if git tag --list | grep -q "^${BASE_TAG}$"; then
|
if git tag --list | grep -q "^${BASE_TAG}$"; then
|
||||||
@@ -65,7 +77,6 @@ jobs:
|
|||||||
|
|
||||||
echo "Generated nightly tag: $NIGHTLY_TAG"
|
echo "Generated nightly tag: $NIGHTLY_TAG"
|
||||||
echo "NIGHTLY_TAG=$NIGHTLY_TAG" >> $GITHUB_ENV
|
echo "NIGHTLY_TAG=$NIGHTLY_TAG" >> $GITHUB_ENV
|
||||||
echo "LATEST_PRODUCTION_TAG=$LATEST_STABLE_TAG" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
git tag "$NIGHTLY_TAG"
|
git tag "$NIGHTLY_TAG"
|
||||||
git push origin "$NIGHTLY_TAG"
|
git push origin "$NIGHTLY_TAG"
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
|||||||
incrementBy: async () => {
|
incrementBy: async () => {
|
||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
|
pgGetIntItem: async (key) => {
|
||||||
|
const value = store[key];
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pgIncrementBy: async () => {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
getItems: async (keys) => {
|
getItems: async (keys) => {
|
||||||
const values = keys.map((key) => {
|
const values = keys.map((key) => {
|
||||||
const value = store[key];
|
const value = store[key];
|
||||||
|
|||||||
@@ -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 deleteServiceToken = async () => {
|
||||||
const serviceTokenListRes = await testServer.inject({
|
const serviceTokenListRes = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`,
|
url: `/api/v1/projects/${seedData1.project.id}/service-token-data`,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
authorization: `Bearer ${jwtAuthToken}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,8 +314,8 @@ describe("Secret expansion", () => {
|
|||||||
expect(listSecrets.imports).toEqual(
|
expect(listSecrets.imports).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
secretPath: `/__reserve_replication_${secretImportFromProdToDev.id}`,
|
secretPath: "/deep/nested",
|
||||||
environment: seedData1.environment.slug,
|
environment: "prod",
|
||||||
secrets: expect.arrayContaining([
|
secrets: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
secretKey: "NESTED_KEY_1",
|
secretKey: "NESTED_KEY_1",
|
||||||
|
|||||||
@@ -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 { initDbConnection } from "@app/db";
|
||||||
import { queueServiceFactory } from "@app/queue";
|
import { queueServiceFactory } from "@app/queue";
|
||||||
import { keyStoreFactory } from "@app/keystore/keystore";
|
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 { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
@@ -62,7 +63,8 @@ export default {
|
|||||||
|
|
||||||
const smtp = mockSmtpServer();
|
const smtp = mockSmtpServer();
|
||||||
const queue = queueServiceFactory(envCfg, { dbConnectionUrl: envCfg.DB_CONNECTION_URI });
|
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();
|
await queue.initialize();
|
||||||
|
|
||||||
|
|||||||
Generated
+41
-3
@@ -25,6 +25,7 @@
|
|||||||
"@fastify/multipart": "8.3.1",
|
"@fastify/multipart": "8.3.1",
|
||||||
"@fastify/passport": "^2.4.0",
|
"@fastify/passport": "^2.4.0",
|
||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
|
"@fastify/reply-from": "^9.8.0",
|
||||||
"@fastify/request-context": "^5.1.0",
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/static": "^7.0.4",
|
"@fastify/static": "^7.0.4",
|
||||||
@@ -8044,6 +8045,42 @@
|
|||||||
"toad-cache": "^3.3.0"
|
"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": {
|
"node_modules/@fastify/request-context": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz",
|
||||||
@@ -29330,9 +29367,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/toad-cache": {
|
"node_modules/toad-cache": {
|
||||||
"version": "3.3.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
|
||||||
"integrity": "sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==",
|
"integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@
|
|||||||
"@fastify/multipart": "8.3.1",
|
"@fastify/multipart": "8.3.1",
|
||||||
"@fastify/passport": "^2.4.0",
|
"@fastify/passport": "^2.4.0",
|
||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
|
"@fastify/reply-from": "^9.8.0",
|
||||||
"@fastify/request-context": "^5.1.0",
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/static": "^7.0.4",
|
"@fastify/static": "^7.0.4",
|
||||||
|
|||||||
Vendored
+12
-3
@@ -1,13 +1,13 @@
|
|||||||
import "fastify";
|
import "fastify";
|
||||||
|
|
||||||
import { Redis } from "ioredis";
|
import { Cluster, Redis } from "ioredis";
|
||||||
|
|
||||||
import { TUsers } from "@app/db/schemas";
|
import { TUsers } from "@app/db/schemas";
|
||||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
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 { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||||
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-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 { 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 { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
|
||||||
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
|
||||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
|
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 { TServerSentEventsService } from "@app/ee/services/event/event-sse-service";
|
||||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-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 { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template";
|
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 { 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 { 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 { 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 { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types";
|
||||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-types";
|
import { TScimServiceFactory } from "@app/ee/services/scim/scim-types";
|
||||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
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 { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-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 { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-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
|
// identity injection. depending on which kinda of token the information is filled in auth
|
||||||
auth: TAuthMode;
|
auth: TAuthMode;
|
||||||
|
shouldForwardWritesToPrimaryInstance: boolean;
|
||||||
permission: {
|
permission: {
|
||||||
authMethod: ActorAuthMethod;
|
authMethod: ActorAuthMethod;
|
||||||
type: ActorType;
|
type: ActorType;
|
||||||
@@ -194,7 +199,7 @@ declare module "fastify" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
redis: Redis;
|
redis: Redis | Cluster;
|
||||||
services: {
|
services: {
|
||||||
login: TAuthLoginFactory;
|
login: TAuthLoginFactory;
|
||||||
password: TAuthPasswordFactory;
|
password: TAuthPasswordFactory;
|
||||||
@@ -293,6 +298,8 @@ declare module "fastify" {
|
|||||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||||
|
relay: TRelayServiceFactory;
|
||||||
|
gatewayV2: TGatewayV2ServiceFactory;
|
||||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||||
folderCommit: TFolderCommitServiceFactory;
|
folderCommit: TFolderCommitServiceFactory;
|
||||||
pit: TPitServiceFactory;
|
pit: TPitServiceFactory;
|
||||||
@@ -303,6 +310,8 @@ declare module "fastify" {
|
|||||||
bus: TEventBusService;
|
bus: TEventBusService;
|
||||||
sse: TServerSentEventsService;
|
sse: TServerSentEventsService;
|
||||||
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
|
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
|
||||||
|
notification: TNotificationServiceFactory;
|
||||||
|
offlineUsageReport: TOfflineUsageReportServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
Vendored
+50
@@ -101,6 +101,9 @@ import {
|
|||||||
TGateways,
|
TGateways,
|
||||||
TGatewaysInsert,
|
TGatewaysInsert,
|
||||||
TGatewaysUpdate,
|
TGatewaysUpdate,
|
||||||
|
TGatewaysV2,
|
||||||
|
TGatewaysV2Insert,
|
||||||
|
TGatewaysV2Update,
|
||||||
TGitAppInstallSessions,
|
TGitAppInstallSessions,
|
||||||
TGitAppInstallSessionsInsert,
|
TGitAppInstallSessionsInsert,
|
||||||
TGitAppInstallSessionsUpdate,
|
TGitAppInstallSessionsUpdate,
|
||||||
@@ -179,6 +182,9 @@ import {
|
|||||||
TIncidentContacts,
|
TIncidentContacts,
|
||||||
TIncidentContactsInsert,
|
TIncidentContactsInsert,
|
||||||
TIncidentContactsUpdate,
|
TIncidentContactsUpdate,
|
||||||
|
TInstanceRelayConfig,
|
||||||
|
TInstanceRelayConfigInsert,
|
||||||
|
TInstanceRelayConfigUpdate,
|
||||||
TIntegrationAuths,
|
TIntegrationAuths,
|
||||||
TIntegrationAuthsInsert,
|
TIntegrationAuthsInsert,
|
||||||
TIntegrationAuthsUpdate,
|
TIntegrationAuthsUpdate,
|
||||||
@@ -191,6 +197,9 @@ import {
|
|||||||
TInternalKms,
|
TInternalKms,
|
||||||
TInternalKmsInsert,
|
TInternalKmsInsert,
|
||||||
TInternalKmsUpdate,
|
TInternalKmsUpdate,
|
||||||
|
TKeyValueStore,
|
||||||
|
TKeyValueStoreInsert,
|
||||||
|
TKeyValueStoreUpdate,
|
||||||
TKmipClientCertificates,
|
TKmipClientCertificates,
|
||||||
TKmipClientCertificatesInsert,
|
TKmipClientCertificatesInsert,
|
||||||
TKmipClientCertificatesUpdate,
|
TKmipClientCertificatesUpdate,
|
||||||
@@ -230,9 +239,15 @@ import {
|
|||||||
TOrgGatewayConfig,
|
TOrgGatewayConfig,
|
||||||
TOrgGatewayConfigInsert,
|
TOrgGatewayConfigInsert,
|
||||||
TOrgGatewayConfigUpdate,
|
TOrgGatewayConfigUpdate,
|
||||||
|
TOrgGatewayConfigV2,
|
||||||
|
TOrgGatewayConfigV2Insert,
|
||||||
|
TOrgGatewayConfigV2Update,
|
||||||
TOrgMemberships,
|
TOrgMemberships,
|
||||||
TOrgMembershipsInsert,
|
TOrgMembershipsInsert,
|
||||||
TOrgMembershipsUpdate,
|
TOrgMembershipsUpdate,
|
||||||
|
TOrgRelayConfig,
|
||||||
|
TOrgRelayConfigInsert,
|
||||||
|
TOrgRelayConfigUpdate,
|
||||||
TOrgRoles,
|
TOrgRoles,
|
||||||
TOrgRolesInsert,
|
TOrgRolesInsert,
|
||||||
TOrgRolesUpdate,
|
TOrgRolesUpdate,
|
||||||
@@ -290,6 +305,9 @@ import {
|
|||||||
TRateLimit,
|
TRateLimit,
|
||||||
TRateLimitInsert,
|
TRateLimitInsert,
|
||||||
TRateLimitUpdate,
|
TRateLimitUpdate,
|
||||||
|
TRelays,
|
||||||
|
TRelaysInsert,
|
||||||
|
TRelaysUpdate,
|
||||||
TResourceMetadata,
|
TResourceMetadata,
|
||||||
TResourceMetadataInsert,
|
TResourceMetadataInsert,
|
||||||
TResourceMetadataUpdate,
|
TResourceMetadataUpdate,
|
||||||
@@ -530,6 +548,11 @@ import {
|
|||||||
TSecretReminderRecipientsInsert,
|
TSecretReminderRecipientsInsert,
|
||||||
TSecretReminderRecipientsUpdate
|
TSecretReminderRecipientsUpdate
|
||||||
} from "@app/db/schemas/secret-reminder-recipients";
|
} from "@app/db/schemas/secret-reminder-recipients";
|
||||||
|
import {
|
||||||
|
TUserNotifications,
|
||||||
|
TUserNotificationsInsert,
|
||||||
|
TUserNotificationsUpdate
|
||||||
|
} from "@app/db/schemas/user-notifications";
|
||||||
|
|
||||||
declare module "knex" {
|
declare module "knex" {
|
||||||
namespace Knex {
|
namespace Knex {
|
||||||
@@ -1233,6 +1256,17 @@ declare module "knex/types/tables" {
|
|||||||
TSecretScanningResourcesInsert,
|
TSecretScanningResourcesInsert,
|
||||||
TSecretScanningResourcesUpdate
|
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<
|
[TableName.SecretScanningScan]: KnexOriginal.CompositeTableType<
|
||||||
TSecretScanningScans,
|
TSecretScanningScans,
|
||||||
TSecretScanningScansInsert,
|
TSecretScanningScansInsert,
|
||||||
@@ -1254,5 +1288,21 @@ declare module "knex/types/tables" {
|
|||||||
TRemindersRecipientsInsert,
|
TRemindersRecipientsInsert,
|
||||||
TRemindersRecipientsUpdate
|
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
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,6 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
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> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
await dropConstraintIfExists(TableName.AppConnection, "app_connections_orgid_name_unique", knex);
|
||||||
t.dropUnique(["orgId", "name"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.SecretSync, (t) => {
|
await knex.schema.alterTable(TableName.SecretSync, (t) => {
|
||||||
t.dropUnique(["projectId", "name"]);
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(),
|
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>;
|
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||||
|
|||||||
@@ -5,11 +5,13 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const AuditLogStreamsSchema = z.object({
|
export const AuditLogStreamsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
url: z.string(),
|
url: z.string().nullable().optional(),
|
||||||
encryptedHeadersCiphertext: z.string().nullable().optional(),
|
encryptedHeadersCiphertext: z.string().nullable().optional(),
|
||||||
encryptedHeadersIV: z.string().nullable().optional(),
|
encryptedHeadersIV: z.string().nullable().optional(),
|
||||||
encryptedHeadersTag: z.string().nullable().optional(),
|
encryptedHeadersTag: z.string().nullable().optional(),
|
||||||
@@ -17,7 +19,9 @@ export const AuditLogStreamsSchema = z.object({
|
|||||||
encryptedHeadersKeyEncoding: z.string().nullable().optional(),
|
encryptedHeadersKeyEncoding: z.string().nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
provider: z.string().default("custom"),
|
||||||
|
encryptedCredentials: zodBuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAuditLogStreams = z.infer<typeof AuditLogStreamsSchema>;
|
export type TAuditLogStreams = z.infer<typeof AuditLogStreamsSchema>;
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export const AuthTokensSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: 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>;
|
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export const DynamicSecretsSchema = z.object({
|
|||||||
encryptedInput: zodBuffer,
|
encryptedInput: zodBuffer,
|
||||||
projectGatewayId: z.string().uuid().nullable().optional(),
|
projectGatewayId: z.string().uuid().nullable().optional(),
|
||||||
gatewayId: 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>;
|
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||||
|
|||||||
@@ -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(),
|
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
|
||||||
gatewayId: z.string().uuid().nullable().optional(),
|
gatewayId: z.string().uuid().nullable().optional(),
|
||||||
accessTokenPeriod: z.coerce.number().default(0),
|
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>;
|
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ export const IdentityLdapAuthsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
accessTokenPeriod: z.coerce.number().default(0),
|
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>;
|
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-checkpoint-resources";
|
||||||
export * from "./folder-tree-checkpoints";
|
export * from "./folder-tree-checkpoints";
|
||||||
export * from "./gateways";
|
export * from "./gateways";
|
||||||
|
export * from "./gateways-v2";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
export * from "./git-app-org";
|
export * from "./git-app-org";
|
||||||
export * from "./github-org-sync-configs";
|
export * from "./github-org-sync-configs";
|
||||||
@@ -57,10 +58,12 @@ export * from "./identity-token-auths";
|
|||||||
export * from "./identity-ua-client-secrets";
|
export * from "./identity-ua-client-secrets";
|
||||||
export * from "./identity-universal-auths";
|
export * from "./identity-universal-auths";
|
||||||
export * from "./incident-contacts";
|
export * from "./incident-contacts";
|
||||||
|
export * from "./instance-relay-config";
|
||||||
export * from "./integration-auths";
|
export * from "./integration-auths";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
export * from "./internal-certificate-authorities";
|
export * from "./internal-certificate-authorities";
|
||||||
export * from "./internal-kms";
|
export * from "./internal-kms";
|
||||||
|
export * from "./key-value-store";
|
||||||
export * from "./kmip-client-certificates";
|
export * from "./kmip-client-certificates";
|
||||||
export * from "./kmip-clients";
|
export * from "./kmip-clients";
|
||||||
export * from "./kmip-org-configs";
|
export * from "./kmip-org-configs";
|
||||||
@@ -75,7 +78,9 @@ export * from "./models";
|
|||||||
export * from "./oidc-configs";
|
export * from "./oidc-configs";
|
||||||
export * from "./org-bots";
|
export * from "./org-bots";
|
||||||
export * from "./org-gateway-config";
|
export * from "./org-gateway-config";
|
||||||
|
export * from "./org-gateway-config-v2";
|
||||||
export * from "./org-memberships";
|
export * from "./org-memberships";
|
||||||
|
export * from "./org-relay-config";
|
||||||
export * from "./org-roles";
|
export * from "./org-roles";
|
||||||
export * from "./organizations";
|
export * from "./organizations";
|
||||||
export * from "./pki-alerts";
|
export * from "./pki-alerts";
|
||||||
@@ -96,6 +101,7 @@ export * from "./project-user-additional-privilege";
|
|||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./rate-limit";
|
export * from "./rate-limit";
|
||||||
|
export * from "./relays";
|
||||||
export * from "./resource-metadata";
|
export * from "./resource-metadata";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
export * from "./scim-tokens";
|
export * from "./scim-tokens";
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -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",
|
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||||
|
UserNotifications = "user_notifications",
|
||||||
// Gateway
|
// Gateway
|
||||||
OrgGatewayConfig = "org_gateway_config",
|
OrgGatewayConfig = "org_gateway_config",
|
||||||
Gateway = "gateways",
|
Gateway = "gateways",
|
||||||
@@ -178,7 +179,16 @@ export enum TableName {
|
|||||||
SecretScanningConfig = "secret_scanning_configs",
|
SecretScanningConfig = "secret_scanning_configs",
|
||||||
// reminders
|
// reminders
|
||||||
Reminder = "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";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
dynamicSecret: SanitizedDynamicSecretSchema
|
dynamicSecret: SanitizedDynamicSecretSchema.extend({
|
||||||
|
inputs: z.unknown()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -151,7 +153,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
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 { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||||
import { registerAssumePrivilegeRouter } from "./assume-privilege-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 { 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 { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
@@ -24,9 +27,9 @@ import { registerPITRouter } from "./pit-router";
|
|||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerRateLimitRouter } from "./rate-limit-router";
|
import { registerRateLimitRouter } from "./rate-limit-router";
|
||||||
|
import { registerRelayRouter } from "./relay-router";
|
||||||
import { registerSamlRouter } from "./saml-router";
|
import { registerSamlRouter } from "./saml-router";
|
||||||
import { registerScimRouter } from "./scim-router";
|
import { registerScimRouter } from "./scim-router";
|
||||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
|
||||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||||
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
||||||
@@ -46,18 +49,29 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
// org role starts with organization
|
// org role starts with organization
|
||||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||||
await server.register(registerLicenseRouter, { prefix: "/organizations" });
|
await server.register(registerLicenseRouter, { prefix: "/organizations" });
|
||||||
|
|
||||||
|
// depreciated in favour of infisical workspace
|
||||||
await server.register(
|
await server.register(
|
||||||
async (projectRouter) => {
|
async (projectRouter) => {
|
||||||
await projectRouter.register(registerProjectRoleRouter);
|
await projectRouter.register(registerDeprecatedProjectRoleRouter);
|
||||||
await projectRouter.register(registerProjectRouter);
|
await projectRouter.register(registerDeprecatedProjectRouter);
|
||||||
await projectRouter.register(registerTrustedIpRouter);
|
|
||||||
await projectRouter.register(registerAssumePrivilegeRouter);
|
|
||||||
},
|
},
|
||||||
{ prefix: "/workspace" }
|
{ 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(registerSnapshotRouter, { prefix: "/secret-snapshot" });
|
||||||
await server.register(registerPITRouter, { prefix: "/pit" });
|
await server.register(registerPITRouter, { prefix: "/pit" });
|
||||||
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
await server.register(registerDeprecatedSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||||
await server.register(registerSecretApprovalRequestRouter, {
|
await server.register(registerSecretApprovalRequestRouter, {
|
||||||
prefix: "/secret-approval-requests"
|
prefix: "/secret-approval-requests"
|
||||||
});
|
});
|
||||||
@@ -79,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||||
|
await server.register(registerRelayRouter, { prefix: "/relays" });
|
||||||
await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" });
|
await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
@@ -114,7 +129,21 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
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(registerUserAdditionalPrivilegeRouter, { prefix: "/user-project-additional-privilege" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (privilegeRouter) => {
|
async (privilegeRouter) => {
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({ organizationId: z.string().trim() }),
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
querystring: z.object({
|
||||||
|
refreshCache: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ plan: z.any() })
|
200: z.object({ plan: z.any() })
|
||||||
}
|
}
|
||||||
@@ -54,7 +60,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
|||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
orgId: req.params.organizationId
|
orgId: req.params.organizationId,
|
||||||
|
refreshCache: req.query.refreshCache
|
||||||
});
|
});
|
||||||
return { plan };
|
return { plan };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ import { packRules } from "@casl/ability/extra";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
import {
|
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||||
backfillPermissionV1SchemaToV2Schema,
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionV1Schema
|
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
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 { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
|
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:projectSlug/roles",
|
url: "/:projectId/roles",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.ProjectRoles],
|
||||||
description: "Create a project role",
|
description: "Create a project role",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
@@ -29,10 +29,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: slugSchema({ max: 64 })
|
slug: slugSchema({ min: 1, max: 64 })
|
||||||
.refine(
|
.refine(
|
||||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"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),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: SanitizedRoleSchemaV1
|
role: SanitizedRoleSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -56,26 +58,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
filter: {
|
filter: {
|
||||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
type: ProjectRoleServiceIdentifierType.ID,
|
||||||
projectSlug: req.params.projectSlug
|
projectId: req.params.projectId
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...req.body,
|
...req.body,
|
||||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { role };
|
return { role };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:projectSlug/roles/:roleId",
|
url: "/:projectId/roles/:roleId",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.ProjectRoles],
|
||||||
description: "Update a project role",
|
description: "Update a project role",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
@@ -83,24 +86,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
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)
|
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: slugSchema({ max: 64 })
|
slug: slugSchema({ min: 1, max: 64 })
|
||||||
.refine(
|
.refine(
|
||||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"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),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: SanitizedRoleSchemaV1
|
role: SanitizedRoleSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -114,9 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
roleId: req.params.roleId,
|
roleId: req.params.roleId,
|
||||||
data: {
|
data: {
|
||||||
...req.body,
|
...req.body,
|
||||||
permissions: req.body.permissions
|
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { role };
|
return { role };
|
||||||
@@ -125,11 +129,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:projectSlug/roles/:roleId",
|
url: "/:projectId/roles/:roleId",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.ProjectRoles],
|
||||||
description: "Delete a project role",
|
description: "Delete a project role",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
@@ -137,12 +143,12 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
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)
|
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: SanitizedRoleSchemaV1
|
role: SanitizedRoleSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -161,11 +167,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:projectSlug/roles",
|
url: "/:projectId/roles",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.ProjectRoles],
|
||||||
description: "List project role",
|
description: "List project role",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
@@ -173,7 +181,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
|
projectId: z.string().trim().describe(PROJECT_ROLE.LIST.projectId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -189,8 +197,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
filter: {
|
filter: {
|
||||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
type: ProjectRoleServiceIdentifierType.ID,
|
||||||
projectSlug: req.params.projectSlug
|
projectId: req.params.projectId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { roles };
|
return { roles };
|
||||||
@@ -199,18 +207,20 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:projectSlug/roles/slug/:slug",
|
url: "/:projectId/roles/slug/:roleSlug",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.ProjectRoles],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
|
projectId: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectId),
|
||||||
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
|
roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
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,
|
actorOrgId: req.permission.orgId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
filter: {
|
filter: {
|
||||||
type: ProjectRoleServiceIdentifierType.SLUG,
|
type: ProjectRoleServiceIdentifierType.ID,
|
||||||
projectSlug: req.params.projectSlug
|
projectId: req.params.projectId
|
||||||
},
|
},
|
||||||
roleSlug: req.params.slug
|
roleSlug: req.params.roleSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
return { role };
|
return { role };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
import { SecretSnapshotsSchema } from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags, AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
||||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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) => {
|
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/secret-snapshots",
|
url: "/:projectId/secret-snapshots",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -26,7 +26,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.workspaceId)
|
projectId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.projectId)
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment),
|
environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment),
|
||||||
@@ -47,7 +47,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
return { secretSnapshots };
|
return { secretSnapshots };
|
||||||
@@ -56,13 +56,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/secret-snapshots/count",
|
url: "/:projectId/secret-snapshots/count",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
@@ -81,7 +81,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
path: req.query.path
|
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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/audit-logs",
|
url: "/:projectId/kms",
|
||||||
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",
|
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: 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()
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -241,7 +116,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId
|
projectId: req.params.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return kmsKey;
|
return kmsKey;
|
||||||
@@ -250,13 +125,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:workspaceId/kms",
|
url: "/:projectId/kms",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
kms: z.discriminatedUnion("type", [
|
kms: z.discriminatedUnion("type", [
|
||||||
@@ -281,13 +156,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
...req.body
|
...req.body
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.UPDATE_PROJECT_KMS,
|
type: EventType.UPDATE_PROJECT_KMS,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -307,13 +182,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/kms/backup",
|
url: "/:projectId/kms/backup",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -328,12 +203,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId
|
projectId: req.params.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_PROJECT_KMS_BACKUP,
|
type: EventType.GET_PROJECT_KMS_BACKUP,
|
||||||
metadata: {}
|
metadata: {}
|
||||||
@@ -346,13 +221,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:workspaceId/kms/backup",
|
url: "/:projectId/kms/backup",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
backup: z.string().min(1)
|
backup: z.string().min(1)
|
||||||
@@ -374,13 +249,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
backup: req.body.backup
|
backup: req.body.backup
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.LOAD_PROJECT_KMS_BACKUP,
|
type: EventType.LOAD_PROJECT_KMS_BACKUP,
|
||||||
metadata: {}
|
metadata: {}
|
||||||
@@ -393,13 +268,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:workspaceId/migrate-v3",
|
url: "/:projectId/migrate-v3",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
response: {
|
response: {
|
||||||
@@ -415,7 +290,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId
|
projectId: req.params.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return migration;
|
return migration;
|
||||||
|
|||||||
@@ -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: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
environment: z.string().trim().optional(),
|
environment: z.string().trim().optional(),
|
||||||
committer: z.string().trim().optional(),
|
committer: z.string().trim().optional(),
|
||||||
search: z.string().trim().optional(),
|
search: z.string().trim().optional(),
|
||||||
@@ -80,7 +80,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.query,
|
...req.query,
|
||||||
projectId: req.query.workspaceId
|
projectId: req.query.projectId
|
||||||
});
|
});
|
||||||
return { approvals, totalCount };
|
return { approvals, totalCount };
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
policyId: z.string().trim().optional()
|
policyId: z.string().trim().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -113,7 +113,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.query.workspaceId,
|
projectId: req.query.projectId,
|
||||||
policyId: req.query.policyId
|
policyId: req.query.policyId
|
||||||
});
|
});
|
||||||
return { approvals };
|
return { approvals };
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/trusted-ips",
|
url: "/:projectId/trusted-ips",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -27,7 +27,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const trustedIps = await server.services.trustedIp.listIpsByProjectId({
|
const trustedIps = await server.services.trustedIp.listIpsByProjectId({
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
@@ -38,13 +38,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:workspaceId/trusted-ips",
|
url: "/:projectId/trusted-ips",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
ipAddress: z.string().trim(),
|
ipAddress: z.string().trim(),
|
||||||
@@ -61,7 +61,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { trustedIp, project } = await server.services.trustedIp.addProjectIp({
|
const { trustedIp, project } = await server.services.trustedIp.addProjectIp({
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
@@ -86,13 +86,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
url: "/:projectId/trusted-ips/:trustedIpId",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
trustedIpId: z.string().trim()
|
trustedIpId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
@@ -108,7 +108,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { trustedIp, project } = await server.services.trustedIp.updateProjectIp({
|
const { trustedIp, project } = await server.services.trustedIp.updateProjectIp({
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@@ -135,13 +135,13 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:workspaceId/trusted-ips/:trustedIpId",
|
url: "/:projectId/trusted-ips/:trustedIpId",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
trustedIpId: z.string().trim()
|
trustedIpId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -153,7 +153,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { trustedIp, project } = await server.services.trustedIp.deleteProjectIp({
|
const { trustedIp, project } = await server.services.trustedIp.deleteProjectIp({
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
|||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
|
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:projectId/roles",
|
url: "/:projectId/roles",
|
||||||
@@ -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
|
SECRET_SCANNING_REGISTER_ROUTER_MAP
|
||||||
} from "@app/ee/routes/v2/secret-scanning-v2-routers";
|
} 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 { 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) => {
|
export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||||
// org role starts with organization
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (projectRouter) => {
|
async (projectRouter) => {
|
||||||
await projectRouter.register(registerProjectRoleRouter);
|
// this has been depreciated and moved to /api/v1/projects
|
||||||
|
await projectRouter.register(registerDeprecatedProjectRoleRouter);
|
||||||
},
|
},
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
@@ -23,6 +25,10 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
prefix: "/identity-project-additional-privilege"
|
prefix: "/identity-project-additional-privilege"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await server.register(registerGatewayV2Router, { prefix: "/gateways" });
|
||||||
|
|
||||||
|
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (secretRotationV2Router) => {
|
async (secretRotationV2Router) => {
|
||||||
// register generic secret rotation endpoints
|
// register generic secret rotation endpoints
|
||||||
|
|||||||
+4
-6
@@ -19,7 +19,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
schema: {
|
schema: {
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
workspaceId: z.string(),
|
projectId: z.string(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
environment: z.string().optional(),
|
environment: z.string().optional(),
|
||||||
environments: z.string().array().optional(),
|
environments: z.string().array().optional(),
|
||||||
@@ -69,7 +69,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.body.workspaceId,
|
|
||||||
...req.body,
|
...req.body,
|
||||||
name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`,
|
name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`,
|
||||||
enforcementLevel: req.body.enforcementLevel
|
enforcementLevel: req.body.enforcementLevel
|
||||||
@@ -174,7 +173,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -204,7 +203,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.query.workspaceId
|
projectId: req.query.projectId
|
||||||
});
|
});
|
||||||
return { approvals };
|
return { approvals };
|
||||||
}
|
}
|
||||||
@@ -263,7 +262,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
secretPath: z.string().trim().transform(removeTrailingSlash)
|
secretPath: z.string().trim().transform(removeTrailingSlash)
|
||||||
}),
|
}),
|
||||||
@@ -284,7 +283,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.query.workspaceId,
|
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
return { policy };
|
return { policy };
|
||||||
+58
-24
@@ -20,6 +20,8 @@ import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack
|
|||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
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 { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||||
import { TGroupDALFactory } from "../group/group-dal";
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
@@ -67,6 +69,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accessApprovalRequestServiceFactory = ({
|
export const accessApprovalRequestServiceFactory = ({
|
||||||
@@ -84,7 +87,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
kmsService,
|
kmsService,
|
||||||
microsoftTeamsService,
|
microsoftTeamsService,
|
||||||
projectMicrosoftTeamsConfigDAL,
|
projectMicrosoftTeamsConfigDAL,
|
||||||
projectSlackConfigDAL
|
projectSlackConfigDAL,
|
||||||
|
notificationService
|
||||||
}: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => {
|
}: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => {
|
||||||
const $getEnvironmentFromPermissions = (permissions: unknown): string | null => {
|
const $getEnvironmentFromPermissions = (permissions: unknown): string | null => {
|
||||||
if (!Array.isArray(permissions) || permissions.length === 0) {
|
if (!Array.isArray(permissions) || permissions.length === 0) {
|
||||||
@@ -245,7 +249,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
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({
|
await triggerWorkflowIntegrationNotification({
|
||||||
input: {
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||||
subjectLine: "Access Approval Request",
|
subjectLine: "Access Approval Request",
|
||||||
@@ -391,7 +407,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||||
const editorFullName = `${editedByUser.firstName} ${editedByUser.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({
|
await triggerWorkflowIntegrationNotification({
|
||||||
input: {
|
input: {
|
||||||
@@ -422,27 +439,44 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await notificationService.createUserNotifications(
|
||||||
recipients: policy.approvers
|
policy.approvers
|
||||||
.filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id)
|
.filter((approver) => Boolean(approver.userId) && approver.userId !== editedByUser.id)
|
||||||
.map((approver) => approver.email!),
|
.map((approver) => ({
|
||||||
subjectLine: "Access Approval Request Updated",
|
userId: approver.userId!,
|
||||||
substitutions: {
|
orgId: actorOrgId,
|
||||||
projectName: project.name,
|
type: NotificationType.ACCESS_APPROVAL_REQUEST_UPDATED,
|
||||||
requesterFullName,
|
title: "Access Approval Request Updated",
|
||||||
requesterEmail: requestedByUser.email,
|
body: `**${editorFullName}** (${editedByUser.email}) has updated the access request submitted by **${requesterFullName}** (${requestedByUser.email}) for **${secretPath}** in the **${envSlug}** environment for project **${project.name}**.`,
|
||||||
isTemporary: true,
|
link: approvalPath
|
||||||
expiresIn: msFn(ms(temporaryRange || ""), { long: true }),
|
}))
|
||||||
secretPath,
|
);
|
||||||
environment: envSlug,
|
|
||||||
permissions: accessTypes,
|
const recipients = policy.approvers
|
||||||
approvalUrl,
|
.filter((approver) => Boolean(approver.email) && approver.userId !== editedByUser.id)
|
||||||
editNote,
|
.map((approver) => approver.email!);
|
||||||
editorFullName,
|
|
||||||
editorEmail: editedByUser.email
|
if (recipients.length > 0) {
|
||||||
},
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.AccessApprovalRequestUpdated
|
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;
|
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) {
|
import { TAuditLogStreams } from "@app/db/schemas";
|
||||||
const { hostname } = new URL(url);
|
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) {
|
export const listProviderOptions = () => {
|
||||||
case "http-intake.logs.datadoghq.com":
|
return [
|
||||||
case "http-intake.logs.us3.datadoghq.com":
|
getDatadogProviderListItem(),
|
||||||
case "http-intake.logs.us5.datadoghq.com":
|
getSplunkProviderListItem(),
|
||||||
case "http-intake.logs.datadoghq.eu":
|
getCustomProviderListItem(),
|
||||||
case "http-intake.logs.ap1.datadoghq.com":
|
getAzureProviderListItem(),
|
||||||
case "http-intake.logs.ddog-gov.com":
|
getCriblProviderListItem()
|
||||||
payload.ddsource = "infisical";
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
payload.service = "audit-logs";
|
};
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { ForbiddenError } from "@casl/ability";
|
||||||
import { RawAxiosRequestHeaders } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { TAuditLogs } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import {
|
||||||
import { request } from "@app/lib/config/request";
|
decryptLogStream,
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
decryptLogStreamCredentials,
|
||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
encryptLogStreamCredentials,
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
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 { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||||
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
|
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
|
||||||
import { providerSpecificPayload } from "./audit-log-stream-fns";
|
import { LogProvider } from "./audit-log-stream-enums";
|
||||||
import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types";
|
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;
|
auditLogStreamDAL: TAuditLogStreamDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAuditLogStreamServiceFactory = ReturnType<typeof auditLogStreamServiceFactory>;
|
||||||
|
|
||||||
export const auditLogStreamServiceFactory = ({
|
export const auditLogStreamServiceFactory = ({
|
||||||
auditLogStreamDAL,
|
auditLogStreamDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
}: TAuditLogStreamServiceFactoryDep): TAuditLogStreamServiceFactory => {
|
kmsService
|
||||||
const create: TAuditLogStreamServiceFactory["create"] = async ({
|
}: TAuditLogStreamServiceFactoryDep) => {
|
||||||
url,
|
const create = async ({ provider, credentials }: TCreateAuditLogStreamDTO, actor: OrgServiceActor) => {
|
||||||
actor,
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
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) {
|
if (!plan.auditLogStreams) {
|
||||||
throw new BadRequestError({
|
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(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor.type,
|
||||||
actorId,
|
actor.id,
|
||||||
actorOrgId,
|
actor.orgId,
|
||||||
actorAuthMethod,
|
actor.authMethod,
|
||||||
actorOrgId
|
actor.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const totalStreams = await auditLogStreamDAL.find({ orgId: actor.orgId });
|
||||||
if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
|
||||||
|
|
||||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
|
||||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message: "Failed to create Audit Log Stream: Plan limit reached. Contact Infisical to increase quota."
|
||||||
"Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams."
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// testing connection first
|
const factory = LOG_STREAM_FACTORY_MAP[provider]();
|
||||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
const validatedCredentials = await factory.validateCredentials({ credentials });
|
||||||
if (headers.length)
|
|
||||||
headers.forEach(({ key, value }) => {
|
|
||||||
streamHeaders[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
await request
|
const encryptedCredentials = await encryptLogStreamCredentials({
|
||||||
.post(
|
credentials: validatedCredentials,
|
||||||
url,
|
orgId: actor.orgId,
|
||||||
{ ...providerSpecificPayload(url), ping: "ok" },
|
kmsService
|
||||||
{
|
});
|
||||||
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 encryptedHeaders = headers
|
|
||||||
? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers))
|
|
||||||
: undefined;
|
|
||||||
const logStream = await auditLogStreamDAL.create({
|
const logStream = await auditLogStreamDAL.create({
|
||||||
orgId: actorOrgId,
|
orgId: actor.orgId,
|
||||||
url,
|
provider,
|
||||||
...(encryptedHeaders
|
encryptedCredentials
|
||||||
? {
|
|
||||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
|
||||||
encryptedHeadersIV: encryptedHeaders.iv,
|
|
||||||
encryptedHeadersTag: encryptedHeaders.tag,
|
|
||||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
|
||||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
});
|
});
|
||||||
return logStream;
|
|
||||||
|
return { ...logStream, credentials: validatedCredentials } as TAuditLogStream;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateById: TAuditLogStreamServiceFactory["updateById"] = async ({
|
const updateById = async (
|
||||||
id,
|
{ logStreamId, provider, credentials }: TUpdateAuditLogStreamDTO,
|
||||||
url,
|
actor: OrgServiceActor
|
||||||
actor,
|
) => {
|
||||||
headers = [],
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
actorId,
|
if (!plan.auditLogStreams) {
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod
|
|
||||||
}) => {
|
|
||||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
|
||||||
if (!plan.auditLogStreams)
|
|
||||||
throw new BadRequestError({
|
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);
|
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||||
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
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);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
const appCfg = getConfig();
|
|
||||||
if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
|
||||||
|
|
||||||
// testing connection first
|
const finalCredentials = { ...credentials };
|
||||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
|
||||||
if (headers.length)
|
|
||||||
headers.forEach(({ key, value }) => {
|
|
||||||
streamHeaders[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
await request
|
// For the "Custom" provider, we must handle masked header values ('******').
|
||||||
.post(
|
// These are placeholders from the frontend for secrets that haven't been changed.
|
||||||
url || logStream.url,
|
// We need to replace them with the original, unmasked values from the database.
|
||||||
{ ...providerSpecificPayload(url || logStream.url), ping: "ok" },
|
if (
|
||||||
{
|
provider === LogProvider.Custom &&
|
||||||
headers: streamHeaders,
|
"headers" in finalCredentials &&
|
||||||
// request timeout
|
Array.isArray(finalCredentials.headers) &&
|
||||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
finalCredentials.headers.some((header) => header.value === "******")
|
||||||
// connection timeout
|
) {
|
||||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
const decryptedOldCredentials = (await decryptLogStreamCredentials({
|
||||||
}
|
encryptedCredentials: logStream.encryptedCredentials,
|
||||||
)
|
orgId: logStream.orgId,
|
||||||
.catch((err) => {
|
kmsService
|
||||||
throw new Error(`Failed to connect with the source ${(err as Error)?.message}`);
|
})) as TCustomProviderCredentials;
|
||||||
});
|
|
||||||
|
|
||||||
const encryptedHeaders = headers
|
const oldHeadersMap = decryptedOldCredentials.headers.reduce<Record<string, string>>((acc, header) => {
|
||||||
? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers))
|
acc[header.key] = header.value;
|
||||||
: undefined;
|
return acc;
|
||||||
const updatedLogStream = await auditLogStreamDAL.updateById(id, {
|
}, {});
|
||||||
url,
|
|
||||||
...(encryptedHeaders
|
const finalHeaders: { key: string; value: string }[] = [];
|
||||||
? {
|
for (const header of finalCredentials.headers) {
|
||||||
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
if (header.value === "******") {
|
||||||
encryptedHeadersIV: encryptedHeaders.iv,
|
const oldValue = oldHeadersMap[header.key];
|
||||||
encryptedHeadersTag: encryptedHeaders.tag,
|
if (oldValue) {
|
||||||
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
finalHeaders.push({ key: header.key, value: oldValue });
|
||||||
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
|
||||||
}
|
}
|
||||||
: {})
|
} 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 ({
|
const deleteById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => {
|
||||||
id,
|
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||||
actor,
|
if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` });
|
||||||
actorId,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod
|
|
||||||
}) => {
|
|
||||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
|
||||||
|
|
||||||
const logStream = await auditLogStreamDAL.findById(id);
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` });
|
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);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const deletedLogStream = await auditLogStreamDAL.deleteById(id);
|
if (logStream.provider !== provider) {
|
||||||
return deletedLogStream;
|
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 ({
|
const getById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => {
|
||||||
id,
|
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||||
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 { orgId } = logStream;
|
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` });
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
|
||||||
|
|
||||||
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(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor.type,
|
||||||
actorId,
|
actor.id,
|
||||||
actorOrgId,
|
logStream.orgId,
|
||||||
actorAuthMethod,
|
actor.authMethod,
|
||||||
actorOrgId
|
actor.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
if (logStream.provider !== provider) {
|
||||||
return logStreams;
|
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 {
|
return {
|
||||||
@@ -244,6 +254,8 @@ export const auditLogStreamServiceFactory = ({
|
|||||||
updateById,
|
updateById,
|
||||||
deleteById,
|
deleteById,
|
||||||
getById,
|
getById,
|
||||||
list
|
list,
|
||||||
|
listProviderOptions,
|
||||||
|
streamLog
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,48 +1,42 @@
|
|||||||
import { TAuditLogStreams } from "@app/db/schemas";
|
import { TAuditLogs } from "@app/db/schemas";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
|
||||||
|
|
||||||
export type LogStreamHeaders = {
|
import { LogProvider } from "./audit-log-stream-enums";
|
||||||
key: string;
|
import { TAzureProvider, TAzureProviderCredentials } from "./azure/azure-provider-types";
|
||||||
value: string;
|
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"> & {
|
export type TUpdateAuditLogStreamDTO = {
|
||||||
url: string;
|
logStreamId: string;
|
||||||
headers?: LogStreamHeaders[];
|
provider: LogProvider;
|
||||||
|
credentials: TAuditLogStreamCredentials;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
export type TLogStreamFactoryValidateCredentials<C extends TAuditLogStreamCredentials> = (input: {
|
||||||
id: string;
|
credentials: C;
|
||||||
url?: string;
|
}) => Promise<C>;
|
||||||
headers?: LogStreamHeaders[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
export type TLogStreamFactoryStreamLog<C extends TAuditLogStreamCredentials> = (input: {
|
||||||
id: string;
|
credentials: C;
|
||||||
};
|
auditLog: TAuditLogs;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
export type TListAuditLogStreamDTO = Omit<TOrgPermission, "orgId">;
|
export type TLogStreamFactory<C extends TAuditLogStreamCredentials> = () => {
|
||||||
|
validateCredentials: TLogStreamFactoryValidateCredentials<C>;
|
||||||
export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
streamLog: TLogStreamFactoryStreamLog<C>;
|
||||||
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[]>;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
|
|
||||||
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 { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
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 { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||||
import { TCreateAuditLogDTO } from "./audit-log-types";
|
import { TCreateAuditLogDTO } from "./audit-log-types";
|
||||||
|
|
||||||
type TAuditLogQueueServiceFactoryDep = {
|
type TAuditLogQueueServiceFactoryDep = {
|
||||||
auditLogDAL: TAuditLogDALFactory;
|
auditLogDAL: TAuditLogDALFactory;
|
||||||
auditLogStreamDAL: Pick<TAuditLogStreamDALFactory, "find">;
|
auditLogStreamService: Pick<TAuditLogStreamServiceFactory, "streamLog">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
@@ -35,7 +27,7 @@ export const auditLogQueueServiceFactory = async ({
|
|||||||
queueService,
|
queueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
auditLogStreamDAL
|
auditLogStreamService
|
||||||
}: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
|
}: TAuditLogQueueServiceFactoryDep): Promise<TAuditLogQueueServiceFactory> => {
|
||||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||||
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
await queueService.queue<QueueName.AuditLog>(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||||
@@ -86,60 +78,7 @@ export const auditLogQueueServiceFactory = async ({
|
|||||||
userAgentType
|
userAgentType
|
||||||
});
|
});
|
||||||
|
|
||||||
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
await auditLogStreamService.streamLog(orgId, auditLog);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export enum EventType {
|
|||||||
MOVE_SECRETS = "move-secrets",
|
MOVE_SECRETS = "move-secrets",
|
||||||
DELETE_SECRET = "delete-secret",
|
DELETE_SECRET = "delete-secret",
|
||||||
DELETE_SECRETS = "delete-secrets",
|
DELETE_SECRETS = "delete-secrets",
|
||||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
GET_PROJECT_KEY = "get-project-key",
|
||||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||||
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
|
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
|
||||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||||
@@ -199,6 +199,7 @@ export enum EventType {
|
|||||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-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_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_SECRETS = "get-identity-universal-auth-client-secret",
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
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",
|
UPDATE_ENVIRONMENT = "update-environment",
|
||||||
DELETE_ENVIRONMENT = "delete-environment",
|
DELETE_ENVIRONMENT = "delete-environment",
|
||||||
GET_ENVIRONMENT = "get-environment",
|
GET_ENVIRONMENT = "get-environment",
|
||||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
ADD_PROJECT_MEMBER = "add-project-member",
|
||||||
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
|
ADD_BATCH_PROJECT_MEMBER = "add-project-members",
|
||||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
REMOVE_PROJECT_MEMBER = "remove-project-member",
|
||||||
CREATE_FOLDER = "create-folder",
|
CREATE_FOLDER = "create-folder",
|
||||||
UPDATE_FOLDER = "update-folder",
|
UPDATE_FOLDER = "update-folder",
|
||||||
DELETE_FOLDER = "delete-folder",
|
DELETE_FOLDER = "delete-folder",
|
||||||
@@ -264,8 +265,8 @@ export enum EventType {
|
|||||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
UPDATE_USER_PROJECT_ROLE = "update-user-project-role",
|
||||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
|
UPDATE_USER_PROJECT_DENIED_PERMISSIONS = "update-user-project-denied-permissions",
|
||||||
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
@@ -392,6 +393,8 @@ export enum EventType {
|
|||||||
CREATE_APP_CONNECTION = "create-app-connection",
|
CREATE_APP_CONNECTION = "create-app-connection",
|
||||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
DELETE_APP_CONNECTION = "delete-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_SHARED_SECRET = "create-shared-secret",
|
||||||
CREATE_SECRET_REQUEST = "create-secret-request",
|
CREATE_SECRET_REQUEST = "create-secret-request",
|
||||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||||
@@ -664,8 +667,8 @@ interface DeleteSecretBatchEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetWorkspaceKeyEvent {
|
interface GetProjectKeyEvent {
|
||||||
type: EventType.GET_WORKSPACE_KEY;
|
type: EventType.GET_PROJECT_KEY;
|
||||||
metadata: {
|
metadata: {
|
||||||
keyId: string;
|
keyId: string;
|
||||||
};
|
};
|
||||||
@@ -1370,6 +1373,10 @@ interface AddIdentityLdapAuthEvent {
|
|||||||
allowedFields?: TAllowedFields[];
|
allowedFields?: TAllowedFields[];
|
||||||
url: string;
|
url: string;
|
||||||
templateId?: string | null;
|
templateId?: string | null;
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDurationSeconds: number;
|
||||||
|
lockoutCounterResetSeconds: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1384,6 +1391,10 @@ interface UpdateIdentityLdapAuthEvent {
|
|||||||
allowedFields?: TAllowedFields[];
|
allowedFields?: TAllowedFields[];
|
||||||
url?: string;
|
url?: string;
|
||||||
templateId?: string | null;
|
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 {
|
interface LoginIdentityOidcAuthEvent {
|
||||||
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1557,24 +1575,24 @@ interface DeleteEnvironmentEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddWorkspaceMemberEvent {
|
interface AddProjectMemberEvent {
|
||||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
type: EventType.ADD_PROJECT_MEMBER;
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddBatchWorkspaceMemberEvent {
|
interface AddBatchProjectMemberEvent {
|
||||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER;
|
type: EventType.ADD_BATCH_PROJECT_MEMBER;
|
||||||
metadata: Array<{
|
metadata: Array<{
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RemoveWorkspaceMemberEvent {
|
interface RemoveProjectMemberEvent {
|
||||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
type: EventType.REMOVE_PROJECT_MEMBER;
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -1713,7 +1731,7 @@ interface DeleteSecretImportEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateUserRole {
|
interface UpdateUserRole {
|
||||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
|
type: EventType.UPDATE_USER_PROJECT_ROLE;
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -1723,7 +1741,7 @@ interface UpdateUserRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateUserDeniedPermissions {
|
interface UpdateUserDeniedPermissions {
|
||||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
|
type: EventType.UPDATE_USER_PROJECT_DENIED_PERMISSIONS;
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: string;
|
userId: string;
|
||||||
email: 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 {
|
interface CreateAppConnectionEvent {
|
||||||
type: EventType.CREATE_APP_CONNECTION;
|
type: EventType.CREATE_APP_CONNECTION;
|
||||||
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
|
metadata: Omit<TCreateAppConnectionDTO, "credentials" | "projectId"> & { connectionId: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateAppConnectionEvent {
|
interface UpdateAppConnectionEvent {
|
||||||
type: EventType.UPDATE_APP_CONNECTION;
|
type: EventType.UPDATE_APP_CONNECTION;
|
||||||
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
|
metadata: Omit<TUpdateAppConnectionDTO, "credentials" | "projectId"> & {
|
||||||
|
connectionId: string;
|
||||||
|
credentialsUpdated: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteAppConnectionEvent {
|
interface DeleteAppConnectionEvent {
|
||||||
@@ -3477,7 +3512,7 @@ export type Event =
|
|||||||
| MoveSecretsEvent
|
| MoveSecretsEvent
|
||||||
| DeleteSecretEvent
|
| DeleteSecretEvent
|
||||||
| DeleteSecretBatchEvent
|
| DeleteSecretBatchEvent
|
||||||
| GetWorkspaceKeyEvent
|
| GetProjectKeyEvent
|
||||||
| AuthorizeIntegrationEvent
|
| AuthorizeIntegrationEvent
|
||||||
| UpdateIntegrationAuthEvent
|
| UpdateIntegrationAuthEvent
|
||||||
| UnauthorizeIntegrationEvent
|
| UnauthorizeIntegrationEvent
|
||||||
@@ -3562,13 +3597,14 @@ export type Event =
|
|||||||
| UpdateIdentityLdapAuthEvent
|
| UpdateIdentityLdapAuthEvent
|
||||||
| GetIdentityLdapAuthEvent
|
| GetIdentityLdapAuthEvent
|
||||||
| RevokeIdentityLdapAuthEvent
|
| RevokeIdentityLdapAuthEvent
|
||||||
|
| ClearIdentityLdapAuthLockoutsEvent
|
||||||
| CreateEnvironmentEvent
|
| CreateEnvironmentEvent
|
||||||
| GetEnvironmentEvent
|
| GetEnvironmentEvent
|
||||||
| UpdateEnvironmentEvent
|
| UpdateEnvironmentEvent
|
||||||
| DeleteEnvironmentEvent
|
| DeleteEnvironmentEvent
|
||||||
| AddWorkspaceMemberEvent
|
| AddProjectMemberEvent
|
||||||
| AddBatchWorkspaceMemberEvent
|
| AddBatchProjectMemberEvent
|
||||||
| RemoveWorkspaceMemberEvent
|
| RemoveProjectMemberEvent
|
||||||
| CreateFolderEvent
|
| CreateFolderEvent
|
||||||
| UpdateFolderEvent
|
| UpdateFolderEvent
|
||||||
| DeleteFolderEvent
|
| DeleteFolderEvent
|
||||||
@@ -3697,6 +3733,8 @@ export type Event =
|
|||||||
| CreateAppConnectionEvent
|
| CreateAppConnectionEvent
|
||||||
| UpdateAppConnectionEvent
|
| UpdateAppConnectionEvent
|
||||||
| DeleteAppConnectionEvent
|
| DeleteAppConnectionEvent
|
||||||
|
| GetAppConnectionUsageEvent
|
||||||
|
| MigrateAppConnectionEvent
|
||||||
| GetSshHostGroupEvent
|
| GetSshHostGroupEvent
|
||||||
| CreateSshHostGroupEvent
|
| CreateSshHostGroupEvent
|
||||||
| UpdateSshHostGroupEvent
|
| UpdateSshHostGroupEvent
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
||||||
try {
|
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);
|
return parseInt(doc || "0", 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
||||||
@@ -55,7 +58,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db)(TableName.DynamicSecretLease)
|
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
||||||
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
||||||
.first()
|
.first()
|
||||||
.join(
|
.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 { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||||
import { TGatewayDALFactory } from "../gateway/gateway-dal";
|
import { TGatewayDALFactory } from "../gateway/gateway-dal";
|
||||||
|
import { TGatewayV2DALFactory } from "../gateway-v2/gateway-v2-dal";
|
||||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||||
import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
|
import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
|
||||||
@@ -39,6 +40,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
|
gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
|
||||||
|
gatewayV2DAL: Pick<TGatewayV2DALFactory, "findOne" | "find">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
gatewayDAL,
|
gatewayDAL,
|
||||||
|
gatewayV2DAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
}: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
|
}: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
|
||||||
const create: TDynamicSecretServiceFactory["create"] = async ({
|
const create: TDynamicSecretServiceFactory["create"] = async ({
|
||||||
@@ -70,6 +73,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
metadata,
|
metadata,
|
||||||
usernameTemplate
|
usernameTemplate
|
||||||
}) => {
|
}) => {
|
||||||
|
let isGatewayV1 = true;
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
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 gatewayId = inputs.gatewayId as string;
|
||||||
|
|
||||||
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
|
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({
|
throw new NotFoundError({
|
||||||
message: `Gateway with ID ${gatewayId} not found`
|
message: `Gateway with ID ${gatewayId} not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!gateway) {
|
||||||
|
isGatewayV1 = false;
|
||||||
|
}
|
||||||
|
|
||||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
gateway.orgId,
|
gateway?.orgId ?? gatewayv2?.orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
@@ -138,7 +147,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
OrgPermissionSubjects.Gateway
|
OrgPermissionSubjects.Gateway
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedGatewayId = gateway.id;
|
selectedGatewayId = gateway?.id ?? gatewayv2?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId });
|
const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId });
|
||||||
@@ -159,7 +168,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
defaultTTL,
|
defaultTTL,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
name,
|
name,
|
||||||
gatewayId: selectedGatewayId,
|
gatewayId: isGatewayV1 ? selectedGatewayId : undefined,
|
||||||
|
gatewayV2Id: isGatewayV1 ? undefined : selectedGatewayId,
|
||||||
usernameTemplate
|
usernameTemplate
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@@ -180,7 +190,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
return cfg;
|
return cfg;
|
||||||
});
|
});
|
||||||
|
|
||||||
return dynamicSecretCfg;
|
return { ...dynamicSecretCfg, inputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
|
const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
|
||||||
@@ -270,20 +280,27 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
|
const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
|
||||||
|
|
||||||
let selectedGatewayId: string | null = null;
|
let selectedGatewayId: string | null = null;
|
||||||
|
let isGatewayV1 = true;
|
||||||
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
|
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
|
||||||
const gatewayId = updatedInput.gatewayId as string;
|
const gatewayId = updatedInput.gatewayId as string;
|
||||||
|
|
||||||
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
|
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({
|
throw new NotFoundError({
|
||||||
message: `Gateway with ID ${gatewayId} not found`
|
message: `Gateway with ID ${gatewayId} not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!gateway) {
|
||||||
|
isGatewayV1 = false;
|
||||||
|
}
|
||||||
|
|
||||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
gateway.orgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
@@ -293,7 +310,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
OrgPermissionSubjects.Gateway
|
OrgPermissionSubjects.Gateway
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedGatewayId = gateway.id;
|
selectedGatewayId = gateway?.id ?? gatewayv2?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(newInput, { projectId });
|
const isConnected = await selectedProvider.validateConnection(newInput, { projectId });
|
||||||
@@ -309,7 +326,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
defaultTTL,
|
defaultTTL,
|
||||||
name: newName ?? name,
|
name: newName ?? name,
|
||||||
status: null,
|
status: null,
|
||||||
gatewayId: selectedGatewayId,
|
gatewayId: isGatewayV1 ? selectedGatewayId : null,
|
||||||
|
gatewayV2Id: isGatewayV1 ? null : selectedGatewayId,
|
||||||
usernameTemplate
|
usernameTemplate
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@@ -337,7 +355,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
return cfg;
|
return cfg;
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedDynamicCfg;
|
return { ...updatedDynamicCfg, inputs: updatedInput };
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
|
const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s
|
|||||||
export const CassandraProvider = (): TDynamicProviderFns => {
|
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
||||||
const hostIps = await Promise.all(
|
await Promise.all(
|
||||||
providerInputs.host
|
providerInputs.host
|
||||||
.split(",")
|
.split(",")
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -48,10 +48,10 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
allowedExpressions: (val) => ["username"].includes(val)
|
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 sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||||
const client = new cassandra.Client({
|
const client = new cassandra.Client({
|
||||||
sslOptions,
|
sslOptions,
|
||||||
@@ -64,7 +64,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
},
|
},
|
||||||
keyspace: providerInputs.keyspace,
|
keyspace: providerInputs.keyspace,
|
||||||
localDataCenter: providerInputs?.localDataCenter,
|
localDataCenter: providerInputs?.localDataCenter,
|
||||||
contactPoints: providerInputs.hostIps
|
contactPoints: providerInputs.host.split(",")
|
||||||
});
|
});
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s
|
|||||||
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
||||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
await verifyHostInputValidity(providerInputs.host);
|
||||||
return { ...providerInputs, hostIp };
|
return { ...providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema> & { hostIp: string }) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||||
const connection = new ElasticSearchClient({
|
const connection = new ElasticSearchClient({
|
||||||
node: {
|
node: {
|
||||||
url: new URL(`${providerInputs.hostIp}:${providerInputs.port}`),
|
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
||||||
...(providerInputs.ca && {
|
...(providerInputs.ca && {
|
||||||
ssl: {
|
ssl: {
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||||
|
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
|
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
@@ -24,12 +25,14 @@ import { VerticaProvider } from "./vertica";
|
|||||||
|
|
||||||
type TBuildDynamicSecretProviderDTO = {
|
type TBuildDynamicSecretProviderDTO = {
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = ({
|
export const buildDynamicSecretProviders = ({
|
||||||
gatewayService
|
gatewayService,
|
||||||
|
gatewayV2Service
|
||||||
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService, gatewayV2Service }),
|
||||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||||
@@ -44,7 +47,7 @@ export const buildDynamicSecretProviders = ({
|
|||||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||||
[DynamicSecretProviders.Totp]: TotpProvider(),
|
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||||
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
||||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService, gatewayV2Service }),
|
||||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
||||||
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
|
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
|
||||||
[DynamicSecretProviders.Github]: GithubProvider(),
|
[DynamicSecretProviders.Github]: GithubProvider(),
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import https from "https";
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { sanitizeString } from "@app/lib/fn";
|
import { sanitizeString } from "@app/lib/fn";
|
||||||
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
|
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||||
|
|
||||||
import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
|
import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
|
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||||
import {
|
import {
|
||||||
DynamicSecretKubernetesSchema,
|
DynamicSecretKubernetesSchema,
|
||||||
KubernetesAuthMethod,
|
KubernetesAuthMethod,
|
||||||
@@ -26,6 +28,7 @@ const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local";
|
|||||||
|
|
||||||
type TKubernetesProviderDTO = {
|
type TKubernetesProviderDTO = {
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateUsername = (usernameTemplate?: string | null) => {
|
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 validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||||
if (!providerInputs.gatewayId && providerInputs.url) {
|
if (!providerInputs.gatewayId && providerInputs.url) {
|
||||||
@@ -58,6 +64,32 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
},
|
},
|
||||||
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||||
): 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 relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
|
||||||
@@ -353,8 +385,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
if (axios.isAxiosError(error)) {
|
||||||
errorMessage = (error.response?.data as { message: string }).message;
|
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({
|
const sanitizedErrorMessage = sanitizeString({
|
||||||
@@ -603,8 +645,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
if (axios.isAxiosError(error)) {
|
||||||
errorMessage = (error.response?.data as { message: string }).message;
|
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({
|
const sanitizedErrorMessage = sanitizeString({
|
||||||
@@ -740,8 +792,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
if (axios.isAxiosError(error)) {
|
||||||
errorMessage = (error.response?.data as { message: string }).message;
|
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({
|
const sanitizedErrorMessage = sanitizeString({
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export const DynamicSecretSqlDBSchema = z.object({
|
|||||||
revocationStatement: z.string().trim(),
|
revocationStatement: z.string().trim(),
|
||||||
renewStatement: z.string().trim().optional(),
|
renewStatement: z.string().trim().optional(),
|
||||||
ca: z.string().optional(),
|
ca: z.string().optional(),
|
||||||
|
sslEnabled: z.boolean().optional(),
|
||||||
gatewayId: z.string().nullable().optional()
|
gatewayId: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,11 +284,11 @@ export const DynamicSecretMongoAtlasSchema = z.object({
|
|||||||
|
|
||||||
export const DynamicSecretMongoDBSchema = z.object({
|
export const DynamicSecretMongoDBSchema = z.object({
|
||||||
host: z.string().min(1).trim().toLowerCase(),
|
host: z.string().min(1).trim().toLowerCase(),
|
||||||
port: z.number().optional(),
|
port: z.number().optional().nullable(),
|
||||||
username: z.string().min(1).trim(),
|
username: z.string().min(1).trim(),
|
||||||
password: z.string().min(1).trim(),
|
password: z.string().min(1).trim(),
|
||||||
database: 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
|
roles: z
|
||||||
.string()
|
.string()
|
||||||
.array()
|
.array()
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ const generateUsername = (usernameTemplate?: string | null, identity?: { name: s
|
|||||||
export const MongoDBProvider = (): TDynamicProviderFns => {
|
export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
||||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
await verifyHostInputValidity(providerInputs.host);
|
||||||
return { ...providerInputs, hostIp };
|
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 isSrv = !providerInputs.port;
|
||||||
const uri = isSrv
|
const uri = isSrv
|
||||||
? `mongodb+srv://${providerInputs.hostIp}`
|
? `mongodb+srv://${providerInputs.host}`
|
||||||
: `mongodb://${providerInputs.hostIp}:${providerInputs.port}`;
|
: `mongodb://${providerInputs.host}:${providerInputs.port}`;
|
||||||
|
|
||||||
const client = new MongoClient(uri, {
|
const client = new MongoClient(uri, {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -44,7 +44,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
password: providerInputs.password
|
password: providerInputs.password
|
||||||
},
|
},
|
||||||
directConnection: !isSrv,
|
directConnection: !isSrv,
|
||||||
ca: providerInputs.ca
|
ca: providerInputs.ca || undefined
|
||||||
});
|
});
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,13 +87,13 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa
|
|||||||
export const RabbitMqProvider = (): TDynamicProviderFns => {
|
export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
|
||||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
await verifyHostInputValidity(providerInputs.host);
|
||||||
return { ...providerInputs, hostIp };
|
return { ...providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema> & { hostIp: string }) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: `${providerInputs.hostIp}:${providerInputs.port}/api`,
|
baseURL: `${providerInputs.host}:${providerInputs.port}/api`,
|
||||||
auth: {
|
auth: {
|
||||||
username: providerInputs.username,
|
username: providerInputs.username,
|
||||||
password: providerInputs.password
|
password: providerInputs.password
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
|||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
||||||
|
|
||||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
await verifyHostInputValidity(providerInputs.host);
|
||||||
validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, {
|
validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, {
|
||||||
allowedExpressions: (val) => ["username", "password"].includes(val)
|
allowedExpressions: (val) => ["username", "password"].includes(val)
|
||||||
});
|
});
|
||||||
@@ -45,16 +45,13 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
|||||||
allowedExpressions: (val) => ["username"].includes(val)
|
allowedExpressions: (val) => ["username"].includes(val)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { ...providerInputs, hostIp };
|
return { ...providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
|
||||||
providerInputs: z.infer<typeof DynamicSecretSapAseSchema> & { hostIp: string },
|
|
||||||
useMaster?: boolean
|
|
||||||
) => {
|
|
||||||
const connectionString =
|
const connectionString =
|
||||||
`DRIVER={FreeTDS};` +
|
`DRIVER={FreeTDS};` +
|
||||||
`SERVER=${providerInputs.hostIp};` +
|
`SERVER=${providerInputs.host};` +
|
||||||
`PORT=${providerInputs.port};` +
|
`PORT=${providerInputs.port};` +
|
||||||
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
|
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
|
||||||
`UID=${providerInputs.username};` +
|
`UID=${providerInputs.username};` +
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
|
||||||
|
|
||||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
await verifyHostInputValidity(providerInputs.host);
|
||||||
validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, {
|
validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, {
|
||||||
allowedExpressions: (val) => ["username", "password", "expiration"].includes(val)
|
allowedExpressions: (val) => ["username", "password", "expiration"].includes(val)
|
||||||
});
|
});
|
||||||
@@ -49,12 +49,12 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, {
|
validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, {
|
||||||
allowedExpressions: (val) => ["username"].includes(val)
|
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({
|
const client = hdb.createClient({
|
||||||
host: providerInputs.hostIp,
|
host: providerInputs.host,
|
||||||
port: providerInputs.port,
|
port: providerInputs.port,
|
||||||
user: providerInputs.username,
|
user: providerInputs.username,
|
||||||
password: providerInputs.password,
|
password: providerInputs.password,
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { sanitizeString } from "@app/lib/fn";
|
import { sanitizeString } from "@app/lib/fn";
|
||||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
|
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
|
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
|
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||||
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
||||||
import { compileUsernameTemplate } from "./templateUtils";
|
import { compileUsernameTemplate } from "./templateUtils";
|
||||||
@@ -128,9 +131,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu
|
|||||||
|
|
||||||
type TSqlDatabaseProviderDTO = {
|
type TSqlDatabaseProviderDTO = {
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
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 validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||||
|
|
||||||
@@ -150,17 +157,40 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
return { ...providerInputs, hostIp };
|
return { ...providerInputs, hostIp };
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
const $getClient = async (
|
||||||
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
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;
|
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({
|
const db = knex({
|
||||||
client: providerInputs.client,
|
client: providerInputs.client,
|
||||||
connection: {
|
connection: {
|
||||||
database: providerInputs.database,
|
database: providerInputs.database,
|
||||||
port: providerInputs.port,
|
port: providerInputs.port,
|
||||||
host: providerInputs.host,
|
host:
|
||||||
user: providerInputs.username,
|
providerInputs.client === SqlProviders.Postgres && !providerInputs.gatewayId
|
||||||
|
? providerInputs.hostIp
|
||||||
|
: providerInputs.host,
|
||||||
|
user: effectiveUser,
|
||||||
password: providerInputs.password,
|
password: providerInputs.password,
|
||||||
ssl,
|
ssl,
|
||||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
// @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
|
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||||
options: isMsSQLClient
|
options: isMsSQLClient
|
||||||
? {
|
? {
|
||||||
|
...(providerInputs.sslEnabled !== undefined ? { encrypt: providerInputs.sslEnabled } : {}),
|
||||||
trustServerCertificate: !providerInputs.ca,
|
trustServerCertificate: !providerInputs.ca,
|
||||||
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
|
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
|
||||||
}
|
}
|
||||||
@@ -183,6 +214,26 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
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 relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
|
||||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
await withGatewayProxy(
|
await withGatewayProxy(
|
||||||
@@ -209,8 +260,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => {
|
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||||
const db = await $getClient({ ...providerInputs, port, host });
|
const db = await $getClient({
|
||||||
|
...providerInputs,
|
||||||
|
port,
|
||||||
|
host,
|
||||||
|
hostIp: providerInputs.hostIp,
|
||||||
|
originalHost: providerInputs.host
|
||||||
|
});
|
||||||
// oracle needs from keyword
|
// oracle needs from keyword
|
||||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
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 password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
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 {
|
try {
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
@@ -294,7 +356,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
const username = entityId;
|
const username = entityId;
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
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 {
|
try {
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
@@ -329,7 +396,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
|||||||
if (!providerInputs.renewStatement) return { entityId };
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
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 expiration = new Date(expireAt).toISOString();
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import Redis from "ioredis";
|
import { Cluster, Redis } from "ioredis";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { BusEventSchema, TopicName } from "./types";
|
import { BusEventSchema, TopicName } from "./types";
|
||||||
|
|
||||||
export const eventBusFactory = (redis: Redis) => {
|
export const eventBusFactory = (redis: Redis | Cluster) => {
|
||||||
const publisher = redis.duplicate();
|
const publisher = redis.duplicate();
|
||||||
// Duplicate the publisher to create a subscriber.
|
// Duplicate the publisher to create a subscriber.
|
||||||
// This is necessary because Redis does not allow a single connection to both publish and subscribe.
|
// This is necessary because Redis does not allow a single connection to both publish and subscribe.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable no-continue */
|
/* eslint-disable no-continue */
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import Redis from "ioredis";
|
import { Cluster, Redis } from "ioredis";
|
||||||
|
|
||||||
import { KeyStorePrefixes } from "@app/keystore/keystore";
|
import { KeyStorePrefixes } from "@app/keystore/keystore";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -12,7 +12,7 @@ import { BusEvent, RegisteredEvent } from "./types";
|
|||||||
const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
const AUTH_REFRESH_INTERVAL = 60 * 1000;
|
||||||
const HEART_BEAT_INTERVAL = 15 * 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 clients = new Set<EventStreamClient>();
|
||||||
|
|
||||||
const heartbeatInterval = setInterval(() => {
|
const heartbeatInterval = setInterval(() => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Readable } from "node:stream";
|
|||||||
|
|
||||||
import { MongoAbility, PureAbility } from "@casl/ability";
|
import { MongoAbility, PureAbility } from "@casl/ability";
|
||||||
import { MongoQuery } from "@ucast/mongo2js";
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
import Redis from "ioredis";
|
import { Cluster, Redis } from "ioredis";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { ProjectType } from "@app/db/schemas";
|
import { ProjectType } from "@app/db/schemas";
|
||||||
@@ -65,7 +65,7 @@ export type EventStreamClient = {
|
|||||||
matcher: PureAbility;
|
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 rules = options.registered.map((r) => {
|
||||||
const secretPath = r.conditions?.secretPath;
|
const secretPath = r.conditions?.secretPath;
|
||||||
const hasConditions = r.conditions?.environmentSlug || 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";
|
||||||
@@ -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