Merge remote-tracking branch 'origin/main' into ENG-3639

This commit is contained in:
Carlos Monastyrski
2025-09-19 20:50:51 -03:00
parent 1c5dd0c35f
commit b2b33b4f8b
806 changed files with 20335 additions and 7209 deletions
@@ -63,6 +63,8 @@ jobs:
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
DD_GIT_REPOSITORY_URL=${{ github.server_url }}/${{ github.repository }}
DD_GIT_COMMIT_SHA=${{ github.sha }}
infisical-fips-standalone: infisical-fips-standalone:
name: Build infisical standalone image postgres name: Build infisical standalone image postgres
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
run: kubectl create namespace infisical-gateway run: kubectl create namespace infisical-gateway
- name: Create gateway secret - name: Create gateway secret
run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token -n infisical-gateway run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token --from-literal=INFISICAL_RELAY_NAME=my-test-relay -n infisical-gateway
- name: Run chart-testing (install) - name: Run chart-testing (install)
run: | run: |
+6
View File
@@ -173,6 +173,12 @@ COPY --from=frontend-runner /app ./backend/frontend-build
ARG INFISICAL_PLATFORM_VERSION ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG DD_GIT_REPOSITORY_URL
ENV DD_GIT_REPOSITORY_URL $DD_GIT_REPOSITORY_URL
ARG DD_GIT_COMMIT_SHA
ENV DD_GIT_COMMIT_SHA $DD_GIT_COMMIT_SHA
ENV PORT 8080 ENV PORT 8080
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV HTTPS_ENABLED false ENV HTTPS_ENABLED false
@@ -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}`
} }
+678
View File
@@ -0,0 +1,678 @@
import { SecretType } from "@app/db/schemas";
import { seedData1 } from "@app/db/seed-data";
import { AuthMode } from "@app/services/auth/auth-type";
type TRawSecret = {
secretKey: string;
secretValue: string;
secretComment?: string;
version: number;
};
const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => {
const createSecretReqBody = {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: dto.type || SecretType.Shared,
secretPath: dto.path,
secretKey: dto.key,
secretValue: dto.value,
secretComment: dto.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v4/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secret");
return createdSecretPayload.secret as TRawSecret;
};
const deleteSecret = async (dto: { path: string; key: string }) => {
const deleteSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v4/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: dto.path
}
});
expect(deleteSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
return updatedSecretPayload.secret as TRawSecret;
};
describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }])(
"Secret V4 - $auth mode",
async ({ auth }) => {
let folderId = "";
let authToken = "";
const secretTestCases = [
{
path: "/",
secret: {
key: "SEC1",
value: "something-secret",
comment: "some comment"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "NESTED-SEC1",
value: "something-secret",
comment: "some comment"
}
},
{
path: "/",
secret: {
key: "secret-key-2",
value: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
omQDpP86RX/hIIQ+JyLSaWYa
-----END PRIVATE KEY-----`,
comment:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "secret-key-3",
value: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
omQDpP86RX/hIIQ+JyLSaWYa
-----END PRIVATE KEY-----`,
comment:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "secret-key-3",
value:
"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gU2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uCg==",
comment: ""
}
}
];
beforeAll(async () => {
if (auth === AuthMode.JWT) {
authToken = jwtAuthToken;
} else if (auth === AuthMode.IDENTITY_ACCESS_TOKEN) {
const identityLogin = await testServer.inject({
method: "POST",
url: "/api/v1/auth/universal-auth/login",
body: {
clientSecret: seedData1.machineIdentity.clientCredentials.secret,
clientId: seedData1.machineIdentity.clientCredentials.id
}
});
expect(identityLogin.statusCode).toBe(200);
authToken = identityLogin.json().accessToken;
}
// create a deep folder
const folderCreate = await testServer.inject({
method: "POST",
url: `/api/v2/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
name: "folder",
path: "/nested1/nested2"
}
});
expect(folderCreate.statusCode).toBe(200);
folderId = folderCreate.json().folder.id;
});
afterAll(async () => {
const deleteFolder = await testServer.inject({
method: "DELETE",
url: `/api/v2/folders/${folderId}`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
path: "/nested1/nested2"
}
});
expect(deleteFolder.statusCode).toBe(200);
});
const getSecrets = async (environment: string, secretPath = "/") => {
const res = await testServer.inject({
method: "GET",
url: `/api/v4/secrets`,
headers: {
authorization: `Bearer ${authToken}`
},
query: {
secretPath,
environment,
projectId: seedData1.projectV3.id
}
});
const secrets: TRawSecret[] = JSON.parse(res.payload).secrets || [];
return secrets;
};
test.each(secretTestCases)("Create secret in path $path", async ({ secret, path }) => {
const createdSecret = await createSecret({ path, ...secret });
expect(createdSecret.secretKey).toEqual(secret.key);
expect(createdSecret.secretValue).toEqual(secret.value);
expect(createdSecret.secretComment || "").toEqual(secret.comment);
expect(createdSecret.version).toEqual(1);
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
secretValue: secret.value,
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key });
});
test.each(secretTestCases)("Get secret by name in path $path", async ({ secret, path }) => {
await createSecret({ path, ...secret });
const getSecByNameRes = await testServer.inject({
method: "GET",
url: `/api/v4/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${authToken}`
},
query: {
secretPath: path,
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug
}
});
expect(getSecByNameRes.statusCode).toBe(200);
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
expect(getSecretByNamePayload).toHaveProperty("secret");
const decryptedSecret = getSecretByNamePayload.secret as TRawSecret;
expect(decryptedSecret.secretKey).toEqual(secret.key);
expect(decryptedSecret.secretValue).toEqual(secret.value);
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
await deleteSecret({ path, key: secret.key });
});
if (auth === AuthMode.JWT) {
test.each(secretTestCases)(
"Creating personal secret without shared throw error in path $path",
async ({ secret }) => {
const createSecretReqBody = {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: SecretType.Personal,
secretKey: secret.key,
secretValue: secret.value,
secretComment: secret.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v4/secrets/SEC2`,
headers: {
authorization: `Bearer ${authToken}`
},
body: createSecretReqBody
});
const payload = JSON.parse(createSecRes.payload);
expect(createSecRes.statusCode).toBe(400);
expect(payload.error).toEqual("BadRequest");
}
);
test.each(secretTestCases)("Creating personal secret in path $path", async ({ secret, path }) => {
await createSecret({ path, ...secret });
const createSecretReqBody = {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: SecretType.Personal,
secretPath: path,
secretKey: secret.key,
secretValue: "personal-value",
secretComment: secret.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v4/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${authToken}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
// list secrets should contain personal one and shared one
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
secretValue: secret.value,
type: SecretType.Shared
}),
expect.objectContaining({
secretKey: secret.key,
secretValue: "personal-value",
type: SecretType.Personal
})
])
);
await deleteSecret({ path, key: secret.key });
});
test.each(secretTestCases)(
"Deleting personal one should not delete shared secret in path $path",
async ({ secret, path }) => {
await createSecret({ path, ...secret }); // shared one
await createSecret({ path, ...secret, type: SecretType.Personal });
// shared secret deletion should delete personal ones also
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
type: SecretType.Shared
}),
expect.not.objectContaining({
secretKey: secret.key,
type: SecretType.Personal
})
])
);
await deleteSecret({ path, key: secret.key });
}
);
}
test.each(secretTestCases)("Update secret in path $path", async ({ path, secret }) => {
await createSecret({ path, ...secret });
const updateSecretReqBody = {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: path,
secretKey: secret.key,
secretValue: "new-value",
secretComment: secret.comment
};
const updateSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v4/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${authToken}`
},
body: updateSecretReqBody
});
expect(updateSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
const decryptedSecret = updatedSecretPayload.secret;
expect(decryptedSecret.secretKey).toEqual(secret.key);
expect(decryptedSecret.secretValue).toEqual("new-value");
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
// list secret should have updated value
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
secretValue: "new-value",
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key });
});
test.each(secretTestCases)("Delete secret in path $path", async ({ secret, path }) => {
await createSecret({ path, ...secret });
const deletedSecret = await deleteSecret({ path, key: secret.key });
expect(deletedSecret.secretKey).toEqual(secret.key);
// shared secret deletion should delete personal ones also
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
secretKey: secret.key,
type: SecretType.Shared
}),
expect.objectContaining({
secretKey: secret.key,
type: SecretType.Personal
})
])
);
});
test.each(secretTestCases)("Bulk create secrets in path $path", async ({ secret, path }) => {
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v4/secrets/batch`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: secret.value,
secretComment: secret.comment
}))
}
});
expect(createSharedSecRes.statusCode).toBe(200);
const createSharedSecPayload = JSON.parse(createSharedSecRes.payload);
expect(createSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: secret.value,
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
);
});
test.each(secretTestCases)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
await createSecret({ ...secret, key: `BULK-${secret.key}-1`, path });
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v4/secrets/batch`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: secret.value,
secretComment: secret.comment
}))
}
});
expect(createSharedSecRes.statusCode).toBe(400);
await deleteSecret({ path, key: `BULK-${secret.key}-1` });
});
test.each(secretTestCases)("Bulk update secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
);
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v4/secrets/batch`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: secret.comment
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
);
});
test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => {
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v4/secrets/batch`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
mode: "upsert",
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: secret.comment
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
);
});
test("Bulk upsert secrets in path multiple paths", async () => {
const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: "comment",
secretPath: secretTestCases[0].path
}));
const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: "comment",
secretPath: secretTestCases[1].path
}));
const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets];
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v4/secrets/batch`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
mode: "upsert",
secrets: testSecrets
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path);
expect(firstBatchSecretsOnInfisical).toEqual(
expect.arrayContaining(
firstBatchSecrets.map((el) =>
expect.objectContaining({
secretKey: el.secretKey,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path);
expect(secondBatchSecretsOnInfisical).toEqual(
expect.arrayContaining(
secondBatchSecrets.map((el) =>
expect.objectContaining({
secretKey: el.secretKey,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey })));
});
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
);
const deletedSharedSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v4/secrets/batch`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
projectId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`
}))
}
});
expect(deletedSharedSecRes.statusCode).toBe(200);
const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload);
expect(deletedSecretPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.value}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
});
}
);
@@ -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,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);
}
}
}
+2 -1
View File
@@ -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>;
@@ -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>;
@@ -1,5 +1,9 @@
import { z } from "zod"; import { z } from "zod";
import {
AzureProviderListItemSchema,
SanitizedAzureProviderSchema
} from "@app/ee/services/audit-log-stream/azure/azure-provider-schemas";
import { import {
CriblProviderListItemSchema, CriblProviderListItemSchema,
SanitizedCriblProviderSchema SanitizedCriblProviderSchema
@@ -24,6 +28,7 @@ const SanitizedAuditLogStreamSchema = z.union([
SanitizedCustomProviderSchema, SanitizedCustomProviderSchema,
SanitizedDatadogProviderSchema, SanitizedDatadogProviderSchema,
SanitizedSplunkProviderSchema, SanitizedSplunkProviderSchema,
SanitizedAzureProviderSchema,
SanitizedCriblProviderSchema SanitizedCriblProviderSchema
]); ]);
@@ -31,6 +36,7 @@ const ProviderOptionsSchema = z.discriminatedUnion("provider", [
CustomProviderListItemSchema, CustomProviderListItemSchema,
DatadogProviderListItemSchema, DatadogProviderListItemSchema,
SplunkProviderListItemSchema, SplunkProviderListItemSchema,
AzureProviderListItemSchema,
CriblProviderListItemSchema CriblProviderListItemSchema
]); ]);
@@ -1,4 +1,9 @@
import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums"; 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 { import {
CreateCriblProviderLogStreamSchema, CreateCriblProviderLogStreamSchema,
SanitizedCriblProviderSchema, SanitizedCriblProviderSchema,
@@ -26,6 +31,15 @@ export * from "./audit-log-stream-router";
export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record<LogProvider, (server: FastifyZodProvider) => Promise<void>> = 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) => { [LogProvider.Custom]: async (server: FastifyZodProvider) => {
registerAuditLogStreamEndpoints({ registerAuditLogStreamEndpoints({
server, server,
@@ -0,0 +1,342 @@
import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
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 stringifiedPermissions = JSON.stringify(
packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))
);
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: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
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 stringifiedPermissions = req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined;
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: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
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
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
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 };
}
});
};
+19 -6
View File
@@ -5,6 +5,9 @@ import { registerAccessApprovalRequestRouter } from "./access-approval-request-r
import { registerAssumePrivilegeRouter } from "./assume-privilege-router"; import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers"; 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";
@@ -27,7 +30,6 @@ import { registerRateLimitRouter } from "./rate-limit-router";
import { registerRelayRouter } from "./relay-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";
@@ -47,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"
}); });
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
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";
@@ -42,6 +43,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod, req.permission.authMethod,
req.permission.orgId req.permission.orgId
); );
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_ORG_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: JSON.stringify(req.body.permissions)
}
}
});
return { role }; return { role };
} }
}); });
@@ -116,6 +133,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod, req.permission.authMethod,
req.permission.orgId req.permission.orgId
); );
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_ORG_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined
}
}
});
return { role }; return { role };
} }
}); });
@@ -146,6 +179,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod, req.permission.authMethod,
req.permission.orgId req.permission.orgId
); );
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_ORG_ROLE,
metadata: { roleId: role.id, slug: role.slug, name: role.name }
}
});
return { role }; return { role };
} }
}); });
+101 -39
View File
@@ -2,26 +2,27 @@ 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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
backfillPermissionV1SchemaToV2Schema, import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
ProjectPermissionV1Schema import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
} from "@app/ee/services/permission/project-permission"; import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
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 +30,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,28 +41,48 @@ 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
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
const role = await server.services.projectRole.createRole({ const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
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: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
} }
}); });
@@ -71,11 +92,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
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,29 +106,33 @@ 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
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
const role = await server.services.projectRole.updateRole({ const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -114,22 +141,39 @@ 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: stringifiedPermissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
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 +181,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
}) })
} }
}, },
@@ -155,17 +199,34 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
roleId: req.params.roleId roleId: req.params.roleId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role }; return { role };
} }
}); });
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 +234,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 +250,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 +260,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 +285,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 };
} }
}); });
+28 -153
View File
@@ -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;
+53 -7
View File
@@ -1,9 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { RelaysSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { 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 { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -89,14 +90,59 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
throw new BadRequestError({
message: "Org relay registration is not yet supported"
});
return server.services.relay.registerRelay({ return server.services.relay.registerRelay({
...req.body, ...req.body,
identityId: req.permission.id, identityId: req.permission.id,
orgId: req.permission.orgId orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
}
});
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: RelaysSchema.array()
}
},
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.getRelays({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: RelaysSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.deleteRelay({
id: req.params.id,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: 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 };
+12 -12
View File
@@ -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,
@@ -2,6 +2,7 @@ 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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs"; import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
@@ -12,7 +13,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",
@@ -52,6 +53,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
const role = await server.services.projectRole.createRole({ const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -63,9 +66,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}, },
data: { data: {
...req.body, ...req.body,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: stringifiedPermissions
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
@@ -112,6 +132,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
const role = await server.services.projectRole.updateRole({ const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -120,9 +141,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: req.params.roleId, roleId: req.params.roleId,
data: { data: {
...req.body, ...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined permissions: stringifiedPermissions
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
@@ -161,6 +199,21 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
roleId: req.params.roleId roleId: req.params.roleId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role }; return { role };
} }
}); });
+6 -3
View File
@@ -7,15 +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 { 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" }
); );
@@ -26,6 +27,8 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerGatewayV2Router, { prefix: "/gateways" }); 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
@@ -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 };
@@ -777,6 +777,20 @@ export const accessApprovalRequestServiceFactory = ({
.map((appUser) => appUser.email) .map((appUser) => appUser.email)
.filter((email): email is string => !!email); .filter((email): email is string => !!email);
const approvalPath = `/projects/secret-management/${project.id}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
await notificationService.createUserNotifications(
approverUsersForEmail.map((approver) => ({
userId: approver.id,
orgId: actorOrgId,
type: NotificationType.ACCESS_POLICY_BYPASSED,
title: "Secret Access Policy Bypassed",
body: `**${actingUser.firstName} ${actingUser.lastName}** (${actingUser.email}) has accessed a secret in **${policy.secretPath || "/"}** in the **${environment?.name || permissionEnvironment}** environment for project **${project.name}** without obtaining the required approval.`,
link: approvalPath
}))
);
if (recipientEmails.length > 0) { if (recipientEmails.length > 0) {
await smtpService.sendMail({ await smtpService.sendMail({
recipients: recipientEmails, recipients: recipientEmails,
@@ -788,7 +802,7 @@ export const accessApprovalRequestServiceFactory = ({
bypassReason: bypassReason || "No reason provided", bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/", secretPath: policy.secretPath || "/",
environment: environment?.name || permissionEnvironment, environment: environment?.name || permissionEnvironment,
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`, approvalUrl,
requestType: "access" requestType: "access"
}, },
template: SmtpTemplates.AccessSecretRequestBypassed template: SmtpTemplates.AccessSecretRequestBypassed
@@ -1,4 +1,5 @@
export enum LogProvider { export enum LogProvider {
Azure = "azure",
Cribl = "cribl", Cribl = "cribl",
Custom = "custom", Custom = "custom",
Datadog = "datadog", Datadog = "datadog",
@@ -1,5 +1,6 @@
import { LogProvider } from "./audit-log-stream-enums"; import { LogProvider } from "./audit-log-stream-enums";
import { TAuditLogStreamCredentials, TLogStreamFactory } from "./audit-log-stream-types"; import { TAuditLogStreamCredentials, TLogStreamFactory } from "./audit-log-stream-types";
import { AzureProviderFactory } from "./azure/azure-provider-factory";
import { CriblProviderFactory } from "./cribl/cribl-provider-factory"; import { CriblProviderFactory } from "./cribl/cribl-provider-factory";
import { CustomProviderFactory } from "./custom/custom-provider-factory"; import { CustomProviderFactory } from "./custom/custom-provider-factory";
import { DatadogProviderFactory } from "./datadog/datadog-provider-factory"; import { DatadogProviderFactory } from "./datadog/datadog-provider-factory";
@@ -8,6 +9,7 @@ import { SplunkProviderFactory } from "./splunk/splunk-provider-factory";
type TLogStreamFactoryImplementation = TLogStreamFactory<TAuditLogStreamCredentials>; type TLogStreamFactoryImplementation = TLogStreamFactory<TAuditLogStreamCredentials>;
export const LOG_STREAM_FACTORY_MAP: Record<LogProvider, TLogStreamFactoryImplementation> = { export const LOG_STREAM_FACTORY_MAP: Record<LogProvider, TLogStreamFactoryImplementation> = {
[LogProvider.Azure]: AzureProviderFactory as TLogStreamFactoryImplementation,
[LogProvider.Datadog]: DatadogProviderFactory as TLogStreamFactoryImplementation, [LogProvider.Datadog]: DatadogProviderFactory as TLogStreamFactoryImplementation,
[LogProvider.Splunk]: SplunkProviderFactory as TLogStreamFactoryImplementation, [LogProvider.Splunk]: SplunkProviderFactory as TLogStreamFactoryImplementation,
[LogProvider.Custom]: CustomProviderFactory as TLogStreamFactoryImplementation, [LogProvider.Custom]: CustomProviderFactory as TLogStreamFactoryImplementation,
@@ -3,6 +3,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TAuditLogStream, TAuditLogStreamCredentials } from "./audit-log-stream-types"; import { TAuditLogStream, TAuditLogStreamCredentials } from "./audit-log-stream-types";
import { getAzureProviderListItem } from "./azure/azure-provider-fns";
import { getCriblProviderListItem } from "./cribl/cribl-provider-fns"; import { getCriblProviderListItem } from "./cribl/cribl-provider-fns";
import { getCustomProviderListItem } from "./custom/custom-provider-fns"; import { getCustomProviderListItem } from "./custom/custom-provider-fns";
import { getDatadogProviderListItem } from "./datadog/datadog-provider-fns"; import { getDatadogProviderListItem } from "./datadog/datadog-provider-fns";
@@ -13,6 +14,7 @@ export const listProviderOptions = () => {
getDatadogProviderListItem(), getDatadogProviderListItem(),
getSplunkProviderListItem(), getSplunkProviderListItem(),
getCustomProviderListItem(), getCustomProviderListItem(),
getAzureProviderListItem(),
getCriblProviderListItem() getCriblProviderListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -1,17 +1,19 @@
import { TAuditLogs } from "@app/db/schemas"; import { TAuditLogs } from "@app/db/schemas";
import { LogProvider } from "./audit-log-stream-enums"; import { LogProvider } from "./audit-log-stream-enums";
import { TAzureProvider, TAzureProviderCredentials } from "./azure/azure-provider-types";
import { TCriblProvider, TCriblProviderCredentials } from "./cribl/cribl-provider-types"; import { TCriblProvider, TCriblProviderCredentials } from "./cribl/cribl-provider-types";
import { TCustomProvider, TCustomProviderCredentials } from "./custom/custom-provider-types"; import { TCustomProvider, TCustomProviderCredentials } from "./custom/custom-provider-types";
import { TDatadogProvider, TDatadogProviderCredentials } from "./datadog/datadog-provider-types"; import { TDatadogProvider, TDatadogProviderCredentials } from "./datadog/datadog-provider-types";
import { TSplunkProvider, TSplunkProviderCredentials } from "./splunk/splunk-provider-types"; import { TSplunkProvider, TSplunkProviderCredentials } from "./splunk/splunk-provider-types";
export type TAuditLogStream = TDatadogProvider | TSplunkProvider | TCustomProvider | TCriblProvider; export type TAuditLogStream = TDatadogProvider | TSplunkProvider | TCustomProvider | TAzureProvider | TCriblProvider;
export type TAuditLogStreamCredentials = export type TAuditLogStreamCredentials =
| TDatadogProviderCredentials | TDatadogProviderCredentials
| TSplunkProviderCredentials | TSplunkProviderCredentials
| TCustomProviderCredentials | TCustomProviderCredentials
| TAzureProviderCredentials
| TCriblProviderCredentials; | TCriblProviderCredentials;
export type TCreateAuditLogStreamDTO = { export type TCreateAuditLogStreamDTO = {
@@ -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>;
@@ -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",
@@ -475,9 +478,21 @@ export enum EventType {
UPDATE_PROJECT = "update-project", UPDATE_PROJECT = "update-project",
DELETE_PROJECT = "delete-project", DELETE_PROJECT = "delete-project",
CREATE_PROJECT_ROLE = "create-project-role",
UPDATE_PROJECT_ROLE = "update-project-role",
DELETE_PROJECT_ROLE = "delete-project-role",
CREATE_ORG_ROLE = "create-org-role",
UPDATE_ORG_ROLE = "update-org-role",
DELETE_ORG_ROLE = "delete-org-role",
CREATE_SECRET_REMINDER = "create-secret-reminder", CREATE_SECRET_REMINDER = "create-secret-reminder",
GET_SECRET_REMINDER = "get-secret-reminder", GET_SECRET_REMINDER = "get-secret-reminder",
DELETE_SECRET_REMINDER = "delete-secret-reminder" DELETE_SECRET_REMINDER = "delete-secret-reminder",
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value"
} }
export const filterableSecretEvents: EventType[] = [ export const filterableSecretEvents: EventType[] = [
@@ -588,6 +603,7 @@ interface CreateSecretEvent {
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretTags?: string[];
}; };
} }
@@ -602,6 +618,7 @@ interface CreateSecretBatchEvent {
secretPath?: string; secretPath?: string;
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretTags?: string[];
}>; }>;
}; };
} }
@@ -615,6 +632,7 @@ interface UpdateSecretEvent {
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretTags?: string[];
}; };
} }
@@ -629,6 +647,7 @@ interface UpdateSecretBatchEvent {
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretPath?: string; secretPath?: string;
secretTags?: string[];
}>; }>;
}; };
} }
@@ -664,8 +683,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 +1389,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 +1407,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 +1428,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 +1591,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 +1747,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 +1757,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 +2815,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 {
@@ -3467,6 +3518,96 @@ interface ProjectDeleteEvent {
}; };
} }
interface DashboardListSecretsEvent {
type: EventType.DASHBOARD_LIST_SECRETS;
metadata: {
environment: string;
secretPath: string;
numberOfSecrets: number;
secretIds: string[];
};
}
interface DashboardGetSecretValueEvent {
type: EventType.DASHBOARD_GET_SECRET_VALUE;
metadata: {
secretId: string;
secretKey: string;
environment: string;
secretPath: string;
};
}
interface DashboardGetSecretVersionValueEvent {
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE;
metadata: {
secretId: string;
version: string;
};
}
interface ProjectRoleCreateEvent {
type: EventType.CREATE_PROJECT_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
description?: string | null;
permissions: string;
};
}
interface ProjectRoleUpdateEvent {
type: EventType.UPDATE_PROJECT_ROLE;
metadata: {
roleId: string;
slug?: string;
name?: string;
description?: string | null;
permissions?: string;
};
}
interface ProjectRoleDeleteEvent {
type: EventType.DELETE_PROJECT_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
};
}
interface OrgRoleCreateEvent {
type: EventType.CREATE_ORG_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
description?: string | null;
permissions: string;
};
}
interface OrgRoleUpdateEvent {
type: EventType.UPDATE_ORG_ROLE;
metadata: {
roleId: string;
slug?: string;
name?: string;
description?: string | null;
permissions?: string;
};
}
interface OrgRoleDeleteEvent {
type: EventType.DELETE_ORG_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -3477,7 +3618,7 @@ export type Event =
| MoveSecretsEvent | MoveSecretsEvent
| DeleteSecretEvent | DeleteSecretEvent
| DeleteSecretBatchEvent | DeleteSecretBatchEvent
| GetWorkspaceKeyEvent | GetProjectKeyEvent
| AuthorizeIntegrationEvent | AuthorizeIntegrationEvent
| UpdateIntegrationAuthEvent | UpdateIntegrationAuthEvent
| UnauthorizeIntegrationEvent | UnauthorizeIntegrationEvent
@@ -3562,13 +3703,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 +3839,8 @@ export type Event =
| CreateAppConnectionEvent | CreateAppConnectionEvent
| UpdateAppConnectionEvent | UpdateAppConnectionEvent
| DeleteAppConnectionEvent | DeleteAppConnectionEvent
| GetAppConnectionUsageEvent
| MigrateAppConnectionEvent
| GetSshHostGroupEvent | GetSshHostGroupEvent
| CreateSshHostGroupEvent | CreateSshHostGroupEvent
| UpdateSshHostGroupEvent | UpdateSshHostGroupEvent
@@ -3780,4 +3924,13 @@ export type Event =
| ProjectDeleteEvent | ProjectDeleteEvent
| SecretReminderCreateEvent | SecretReminderCreateEvent
| SecretReminderGetEvent | SecretReminderGetEvent
| SecretReminderDeleteEvent; | SecretReminderDeleteEvent
| DashboardListSecretsEvent
| DashboardGetSecretValueEvent
| DashboardGetSecretVersionValueEvent
| ProjectRoleCreateEvent
| ProjectRoleUpdateEvent
| ProjectRoleDeleteEvent
| OrgRoleCreateEvent
| OrgRoleUpdateEvent
| OrgRoleDeleteEvent;
@@ -16,7 +16,7 @@ import {
PutUserPolicyCommand, PutUserPolicyCommand,
RemoveUserFromGroupCommand RemoveUserFromGroupCommand
} from "@aws-sdk/client-iam"; } from "@aws-sdk/client-iam";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; import { AssumeRoleCommand, GetSessionTokenCommand, STSClient } from "@aws-sdk/client-sts";
import { z } from "zod"; import { z } from "zod";
import { CustomAWSHasher } from "@app/lib/aws/hashing"; import { CustomAWSHasher } from "@app/lib/aws/hashing";
@@ -26,9 +26,12 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn"; import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; import { AwsIamAuthType, AwsIamCredentialType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils"; import { compileUsernameTemplate } from "./templateUtils";
// AWS STS duration constants (in seconds)
const AWS_STS_MIN_DURATION = 900;
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const randomUsername = alphaNumericNanoId(32); const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername; if (!usernameTemplate) return randomUsername;
@@ -120,6 +123,58 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => { const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
try { try {
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
if (providerInputs.method === AwsIamAuthType.AccessKey) {
const stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: providerInputs.accessKey,
secretAccessKey: providerInputs.secretAccessKey
}
});
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
return true;
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
const appCfg = getConfig();
const stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials:
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
? {
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
}
: undefined
});
await stsClient.send(
new AssumeRoleCommand({
RoleArn: providerInputs.roleArn,
RoleSessionName: `infisical-validation-${crypto.nativeCrypto.randomUUID()}`,
DurationSeconds: AWS_STS_MIN_DURATION,
ExternalId: projectId
})
);
return true;
}
if (providerInputs.method === AwsIamAuthType.IRSA) {
const stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher
});
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
return true;
}
}
const client = await $getClient(providerInputs, projectId); const client = await $getClient(providerInputs, projectId);
const isConnected = await client const isConnected = await client
.send(new GetUserCommand({})) .send(new GetUserCommand({}))
@@ -137,7 +192,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
}); });
return isConnected; return isConnected;
} catch (err) { } catch (err) {
const sensitiveTokens = []; const sensitiveTokens: string[] = [];
if (providerInputs.method === AwsIamAuthType.AccessKey) { if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
} }
@@ -163,102 +218,269 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
}; };
metadata: { projectId: string }; metadata: { projectId: string };
}) => { }) => {
const { inputs, usernameTemplate, metadata, identity } = data; const { inputs, usernameTemplate, metadata, identity, expireAt } = data;
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs, metadata.projectId);
const username = generateUsername(usernameTemplate, identity); if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; try {
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }]; let stsClient: STSClient;
let entityId: string;
if (providerInputs.tags && Array.isArray(providerInputs.tags)) { const currentTime = Date.now();
const additionalTags = providerInputs.tags.map((tag) => ({ const requestedDuration = Math.floor((expireAt - currentTime) / 1000);
Key: tag.key,
Value: tag.value if (requestedDuration <= 0) {
})); throw new BadRequestError({ message: "Expiration time must be in the future" });
awsTags.push(...additionalTags); }
let durationSeconds: number;
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
durationSeconds = requestedDuration;
const appCfg = getConfig();
stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials:
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
? {
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
}
: undefined
});
const assumeRoleRes = await stsClient.send(
new AssumeRoleCommand({
RoleArn: providerInputs.roleArn,
RoleSessionName: `infisical-temp-cred-${crypto.nativeCrypto.randomUUID()}`,
DurationSeconds: durationSeconds,
ExternalId: metadata.projectId
})
);
if (
!assumeRoleRes.Credentials?.AccessKeyId ||
!assumeRoleRes.Credentials?.SecretAccessKey ||
!assumeRoleRes.Credentials?.SessionToken
) {
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
}
entityId = `assume-role-${alphaNumericNanoId(8)}`;
return {
entityId,
data: {
ACCESS_KEY: assumeRoleRes.Credentials.AccessKeyId,
SECRET_ACCESS_KEY: assumeRoleRes.Credentials.SecretAccessKey,
SESSION_TOKEN: assumeRoleRes.Credentials.SessionToken
}
};
}
if (providerInputs.method === AwsIamAuthType.AccessKey) {
durationSeconds = requestedDuration;
stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: providerInputs.accessKey,
secretAccessKey: providerInputs.secretAccessKey
}
});
const sessionTokenRes = await stsClient.send(
new GetSessionTokenCommand({
DurationSeconds: durationSeconds
})
);
if (
!sessionTokenRes.Credentials?.AccessKeyId ||
!sessionTokenRes.Credentials?.SecretAccessKey ||
!sessionTokenRes.Credentials?.SessionToken
) {
throw new BadRequestError({ message: "Failed to get session token - verify credentials and permissions" });
}
entityId = `session-token-${alphaNumericNanoId(8)}`;
return {
entityId,
data: {
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
}
};
}
if (providerInputs.method === AwsIamAuthType.IRSA) {
durationSeconds = requestedDuration;
stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher
});
const sessionTokenRes = await stsClient.send(
new GetSessionTokenCommand({
DurationSeconds: durationSeconds
})
);
if (
!sessionTokenRes.Credentials?.AccessKeyId ||
!sessionTokenRes.Credentials?.SecretAccessKey ||
!sessionTokenRes.Credentials?.SessionToken
) {
throw new BadRequestError({
message: "Failed to get session token - verify IRSA credentials and permissions"
});
}
entityId = `irsa-session-${alphaNumericNanoId(8)}`;
return {
entityId,
data: {
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
}
};
}
throw new BadRequestError({ message: "Unsupported authentication method for temporary credentials" });
} catch (err) {
const sensitiveTokens: string[] = [];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
let errorMessage = (err as Error)?.message || "Unknown error";
if (err && typeof err === "object" && "name" in err && "$metadata" in err) {
const awsError = err as { name?: string; message?: string; $metadata?: object };
if (awsError.name) {
errorMessage = `${awsError.name}: ${errorMessage}`;
}
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create temporary credentials: ${sanitizedErrorMessage}`
});
}
} }
try { if (providerInputs.credentialType === AwsIamCredentialType.IamUser) {
const createUserRes = await client.send( const client = await $getClient(providerInputs, metadata.projectId);
new CreateUserCommand({
Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: awsTags,
UserName: username
})
);
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); const username = generateUsername(usernameTemplate, identity);
if (userGroups) { const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
await Promise.all( const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
userGroups
.split(",") if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
.filter(Boolean) const additionalTags = providerInputs.tags.map((tag) => ({
.map((group) => Key: tag.key,
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group })) Value: tag.value
) }));
); awsTags.push(...additionalTags);
} }
if (policyArns) {
await Promise.all( try {
policyArns const createUserRes = await client.send(
.split(",") new CreateUserCommand({
.filter(Boolean) Path: awsPath,
.map((policyArn) => PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
client.send( Tags: awsTags,
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }) UserName: username
)
)
);
}
if (policyDocument) {
await client.send(
new PutUserPolicyCommand({
UserName: createUserRes.User.UserName,
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
PolicyDocument: policyDocument
}) })
); );
}
const createAccessKeyRes = await client.send( if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
new CreateAccessKeyCommand({ if (userGroups) {
UserName: createUserRes.User.UserName await Promise.all(
}) userGroups
); .split(",")
if (!createAccessKeyRes.AccessKey) .filter(Boolean)
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" }); .map((group) =>
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
return { )
entityId: username, );
data: {
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
USERNAME: username
} }
}; if (policyArns) {
} catch (err) { await Promise.all(
const sensitiveTokens = [username]; policyArns
if (providerInputs.method === AwsIamAuthType.AccessKey) { .split(",")
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); .filter(Boolean)
.map((policyArn) =>
client.send(
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
)
)
);
}
if (policyDocument) {
await client.send(
new PutUserPolicyCommand({
UserName: createUserRes.User.UserName,
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
PolicyDocument: policyDocument
})
);
}
const createAccessKeyRes = await client.send(
new CreateAccessKeyCommand({
UserName: createUserRes.User.UserName
})
);
if (!createAccessKeyRes.AccessKey)
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
return {
entityId: username,
data: {
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
USERNAME: username
}
};
} catch (err) {
const sensitiveTokens = [username];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
} }
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
} }
throw new BadRequestError({ message: "Invalid credential type specified" });
}; };
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => { const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
return { entityId };
}
const client = await $getClient(providerInputs, metadata.projectId); const client = await $getClient(providerInputs, metadata.projectId);
const username = entityId; const username = entityId;
@@ -32,6 +32,11 @@ export enum AwsIamAuthType {
IRSA = "irsa" IRSA = "irsa"
} }
export enum AwsIamCredentialType {
IamUser = "iam-user",
TemporaryCredentials = "temporary-credentials"
}
export enum ElasticSearchAuthTypes { export enum ElasticSearchAuthTypes {
User = "user", User = "user",
ApiKey = "api-key" ApiKey = "api-key"
@@ -203,6 +208,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
z.discriminatedUnion("method", [ z.discriminatedUnion("method", [
z.object({ z.object({
method: z.literal(AwsIamAuthType.AccessKey), method: z.literal(AwsIamAuthType.AccessKey),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
accessKey: z.string().trim().min(1), accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1), secretAccessKey: z.string().trim().min(1),
region: z.string().trim().min(1), region: z.string().trim().min(1),
@@ -215,6 +221,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
}), }),
z.object({ z.object({
method: z.literal(AwsIamAuthType.AssumeRole), method: z.literal(AwsIamAuthType.AssumeRole),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
roleArn: z.string().trim().min(1, "Role ARN required"), roleArn: z.string().trim().min(1, "Role ARN required"),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
@@ -226,6 +233,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
}), }),
z.object({ z.object({
method: z.literal(AwsIamAuthType.IRSA), method: z.literal(AwsIamAuthType.IRSA),
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
region: z.string().trim().min(1), region: z.string().trim().min(1),
awsPath: z.string().trim().optional(), awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(), permissionBoundaryPolicyArn: z.string().trim().optional(),
@@ -395,7 +395,8 @@ export const gatewayV2ServiceFactory = ({
relayId: gateway.relayId, relayId: gateway.relayId,
orgId: gateway.orgId, orgId: gateway.orgId,
orgName: gateway.orgName, orgName: gateway.orgName,
gatewayId gatewayId,
gatewayName: gateway.name
}); });
return { return {
@@ -508,7 +509,8 @@ export const gatewayV2ServiceFactory = ({
const relayCredentials = await relayService.getCredentialsForGateway({ const relayCredentials = await relayService.getCredentialsForGateway({
relayName, relayName,
orgId, orgId,
gatewayId: gateway.id gatewayId: gateway.id,
gatewayName: gateway.name
}); });
return { return {
@@ -160,7 +160,10 @@ export const licenseServiceFactory = ({
} }
if (isValidOfflineLicense) { if (isValidOfflineLicense) {
onPremFeatures = contents.license.features; onPremFeatures = {
...contents.license.features,
slug: "enterprise"
};
instanceType = InstanceType.EnterpriseOnPremOffline; instanceType = InstanceType.EnterpriseOnPremOffline;
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`); logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
isValidLicense = true; isValidLicense = true;
@@ -24,7 +24,7 @@ export type TOfflineLicense = {
export type TFeatureSet = { export type TFeatureSet = {
_id: null; _id: null;
slug: null; slug: string | null;
tier: -1; tier: -1;
workspaceLimit: null; workspaceLimit: null;
workspacesUsed: number; workspacesUsed: number;
@@ -2,6 +2,7 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
@@ -264,6 +265,17 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretEvents ProjectPermissionSub.SecretEvents
); );
can(
[
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionAppConnectionActions.Edit,
ProjectPermissionAppConnectionActions.Delete,
ProjectPermissionAppConnectionActions.Read,
ProjectPermissionAppConnectionActions.Connect
],
ProjectPermissionSub.AppConnections
);
return rules; return rules;
}; };
@@ -477,6 +489,8 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.SecretEvents ProjectPermissionSub.SecretEvents
); );
can(ProjectPermissionAppConnectionActions.Connect, ProjectPermissionSub.AppConnections);
return rules; return rules;
}; };
@@ -58,6 +58,13 @@ export enum OrgPermissionGatewayActions {
AttachGateways = "attach-gateways" AttachGateways = "attach-gateways"
} }
export enum OrgPermissionRelayActions {
CreateRelays = "create-relays",
ListRelays = "list-relays",
EditRelays = "edit-relays",
DeleteRelays = "delete-relays"
}
export enum OrgPermissionIdentityActions { export enum OrgPermissionIdentityActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -87,6 +94,7 @@ export enum OrgPermissionBillingActions {
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {
Workspace = "workspace", Workspace = "workspace",
Project = "project",
Role = "role", Role = "role",
Member = "member", Member = "member",
Settings = "settings", Settings = "settings",
@@ -108,6 +116,7 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections", AppConnections = "app-connections",
Kmip = "kmip", Kmip = "kmip",
Gateway = "gateway", Gateway = "gateway",
Relay = "relay",
SecretShare = "secret-share" SecretShare = "secret-share"
} }
@@ -117,6 +126,7 @@ export type AppConnectionSubjectFields = {
export type OrgPermissionSet = export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Create, OrgPermissionSubjects.Project]
| [OrgPermissionActions, OrgPermissionSubjects.Role] | [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Member]
| [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.Settings]
@@ -134,6 +144,7 @@ export type OrgPermissionSet =
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway] | [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionRelayActions, OrgPermissionSubjects.Relay]
| [ | [
OrgPermissionAppConnectionActions, OrgPermissionAppConnectionActions,
( (
@@ -166,6 +177,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
subject: z.literal(OrgPermissionSubjects.Workspace).describe("The entity this permission pertains to."), subject: z.literal(OrgPermissionSubjects.Workspace).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.") action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.")
}), }),
z.object({
subject: z.literal(OrgPermissionSubjects.Project).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.")
}),
z.object({ z.object({
subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."), subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
@@ -273,6 +288,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Relay).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionRelayActions).describe(
"Describe what action an entity can take."
)
}) })
]); ]);
@@ -280,6 +301,7 @@ const buildAdminPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions // ws permissions
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Project);
// role permission // role permission
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
@@ -376,6 +398,11 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
@@ -413,6 +440,7 @@ const buildMemberPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Project);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
@@ -437,6 +465,10 @@ const buildMemberPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate); can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can( can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
@@ -147,6 +147,14 @@ export enum ProjectPermissionSecretScanningDataSourceActions {
ReadResources = "read-data-source-resources" ReadResources = "read-data-source-resources"
} }
export enum ProjectPermissionAppConnectionActions {
Read = "read-app-connections",
Create = "create-app-connections",
Edit = "edit-app-connections",
Delete = "delete-app-connections",
Connect = "connect-app-connections"
}
export enum ProjectPermissionSecretScanningFindingActions { export enum ProjectPermissionSecretScanningFindingActions {
Read = "read-findings", Read = "read-findings",
Update = "update-findings" Update = "update-findings"
@@ -208,7 +216,8 @@ export enum ProjectPermissionSub {
SecretScanningDataSources = "secret-scanning-data-sources", SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings", SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs", SecretScanningConfigs = "secret-scanning-configs",
SecretEvents = "secret-events" SecretEvents = "secret-events",
AppConnections = "app-connections"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -272,6 +281,10 @@ export type PkiSubscriberSubjectFields = {
// (dangtony98): consider adding [commonName] as a subject field in the future // (dangtony98): consider adding [commonName] as a subject field in the future
}; };
export type AppConnectionSubjectFields = {
connectionId: string;
};
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
@@ -365,6 +378,13 @@ export type ProjectPermissionSet =
| [ | [
ProjectPermissionSecretEventActions, ProjectPermissionSecretEventActions,
ProjectPermissionSub.SecretEvents | (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields) ProjectPermissionSub.SecretEvents | (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
]
| [
ProjectPermissionAppConnectionActions,
(
| ProjectPermissionSub.AppConnections
| (ForcedSubject<ProjectPermissionSub.AppConnections> & AppConnectionSubjectFields)
)
]; ];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
@@ -580,6 +600,21 @@ const PkiTemplateConditionSchema = z
}) })
.partial(); .partial();
const AppConnectionConditionSchema = z
.object({
connectionId: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
@@ -760,6 +795,16 @@ const GeneralPermissionSchema = [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningConfigActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretScanningConfigActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}),
z.object({
subject: z.literal(ProjectPermissionSub.AppConnections).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionAppConnectionActions).describe(
"Describe what action an entity can take."
),
conditions: AppConnectionConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}) })
]; ];
+4 -2
View File
@@ -754,7 +754,8 @@ export const pitServiceFactory = ({
secrets: newSecrets.map((secret) => ({ secrets: newSecrets.map((secret) => ({
secretId: secret.id, secretId: secret.id,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version secretVersion: secret.version,
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -781,7 +782,8 @@ export const pitServiceFactory = ({
secrets: updatedSecrets.map((secret) => ({ secrets: updatedSecrets.map((secret) => ({
secretId: secret.id, secretId: secret.id,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version secretVersion: secret.version,
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -0,0 +1 @@
export const RELAY_CONNECTING_GATEWAY_INFO = "1.3.6.1.4.1.12345.100.3";
+130 -9
View File
@@ -1,9 +1,13 @@
import { isIP } from "node:net";
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { TRelays } from "@app/db/schemas"; import { TRelays } from "@app/db/schemas";
import { PgSqlLock } from "@app/keystore/keystore"; import { PgSqlLock } from "@app/keystore/keystore";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns"; import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { import {
@@ -14,11 +18,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionRelayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns"; import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
import { SshCertType } from "../ssh/ssh-certificate-authority-types"; import { SshCertType } from "../ssh/ssh-certificate-authority-types";
import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types"; import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types";
import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal"; import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal";
import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal"; import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal";
import { RELAY_CONNECTING_GATEWAY_INFO } from "./relay-constants";
import { TRelayDALFactory } from "./relay-dal"; import { TRelayDALFactory } from "./relay-dal";
export type TRelayServiceFactory = ReturnType<typeof relayServiceFactory>; export type TRelayServiceFactory = ReturnType<typeof relayServiceFactory>;
@@ -29,12 +37,16 @@ export const relayServiceFactory = ({
instanceRelayConfigDAL, instanceRelayConfigDAL,
orgRelayConfigDAL, orgRelayConfigDAL,
relayDAL, relayDAL,
kmsService kmsService,
licenseService,
permissionService
}: { }: {
instanceRelayConfigDAL: TInstanceRelayConfigDALFactory; instanceRelayConfigDAL: TInstanceRelayConfigDALFactory;
orgRelayConfigDAL: TOrgRelayConfigDALFactory; orgRelayConfigDAL: TOrgRelayConfigDALFactory;
relayDAL: TRelayDALFactory; relayDAL: TRelayDALFactory;
kmsService: TKmsServiceFactory; kmsService: TKmsServiceFactory;
licenseService: TLicenseServiceFactory;
permissionService: TPermissionServiceFactory;
}) => { }) => {
const $getInstanceCAs = async () => { const $getInstanceCAs = async () => {
const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => { const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => {
@@ -639,8 +651,9 @@ export const relayServiceFactory = ({
true true
), ),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
// san // san
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: host }], false) new x509.SubjectAlternativeNameExtension([{ type: isIP(host) ? "ip" : "dns", value: host }], false)
]; ];
const relayServerSerialNumber = createSerialNumber(); const relayServerSerialNumber = createSerialNumber();
@@ -689,6 +702,7 @@ export const relayServiceFactory = ({
const $generateRelayClientCredentials = async ({ const $generateRelayClientCredentials = async ({
gatewayId, gatewayId,
gatewayName,
orgId, orgId,
orgName, orgName,
relayPkiClientCaCertificate, relayPkiClientCaCertificate,
@@ -697,6 +711,7 @@ export const relayServiceFactory = ({
relayPkiServerCaCertificateChain relayPkiServerCaCertificateChain
}: { }: {
gatewayId: string; gatewayId: string;
gatewayName: string;
orgId: string; orgId: string;
orgName: string; orgName: string;
relayPkiClientCaCertificate: Buffer; relayPkiClientCaCertificate: Buffer;
@@ -727,6 +742,16 @@ export const relayServiceFactory = ({
const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
const clientCertSerialNumber = createSerialNumber(); const clientCertSerialNumber = createSerialNumber();
const connectingGatewayInfoExtension = new x509.Extension(
RELAY_CONNECTING_GATEWAY_INFO,
false,
Buffer.from(
JSON.stringify({
name: gatewayName
})
)
);
// Build standard extensions // Build standard extensions
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
@@ -740,7 +765,8 @@ export const relayServiceFactory = ({
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
true true
), ),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true),
connectingGatewayInfoExtension
]; ];
const clientCert = await x509.X509CertificateGenerator.create({ const clientCert = await x509.X509CertificateGenerator.create({
@@ -768,11 +794,13 @@ export const relayServiceFactory = ({
const getCredentialsForGateway = async ({ const getCredentialsForGateway = async ({
relayName, relayName,
orgId, orgId,
gatewayId gatewayId,
gatewayName
}: { }: {
relayName: string; relayName: string;
orgId: string; orgId: string;
gatewayId: string; gatewayId: string;
gatewayName: string;
}) => { }) => {
let relay: TRelays | null = await relayDAL.findOne({ let relay: TRelays | null = await relayDAL.findOne({
orgId, orgId,
@@ -819,10 +847,10 @@ export const relayServiceFactory = ({
const relayClientSshCert = await createSshCert({ const relayClientSshCert = await createSshCert({
caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"), caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"),
clientPublicKey: relayClientSshPublicKey, clientPublicKey: relayClientSshPublicKey,
keyId: `relay-client-${relay.id}`, keyId: `client-${relayName}`,
principals: [gatewayId], principals: [gatewayId, gatewayName],
certType: SshCertType.USER, certType: SshCertType.USER,
requestedTtl: "30d" requestedTtl: "1d"
}); });
return { return {
@@ -837,12 +865,14 @@ export const relayServiceFactory = ({
relayId, relayId,
orgId, orgId,
orgName, orgName,
gatewayId gatewayId,
gatewayName
}: { }: {
relayId: string; relayId: string;
orgId: string; orgId: string;
orgName: string; orgName: string;
gatewayId: string; gatewayId: string;
gatewayName: string;
}) => { }) => {
const relay = await relayDAL.findOne({ const relay = await relayDAL.findOne({
id: relayId id: relayId
@@ -860,6 +890,7 @@ export const relayServiceFactory = ({
const instanceCAs = await $getInstanceCAs(); const instanceCAs = await $getInstanceCAs();
const relayCertificateCredentials = await $generateRelayClientCredentials({ const relayCertificateCredentials = await $generateRelayClientCredentials({
gatewayId, gatewayId,
gatewayName,
orgId, orgId,
orgName, orgName,
relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate, relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate,
@@ -877,6 +908,7 @@ export const relayServiceFactory = ({
const orgCAs = await $getOrgCAs(orgId); const orgCAs = await $getOrgCAs(orgId);
const relayCertificateCredentials = await $generateRelayClientCredentials({ const relayCertificateCredentials = await $generateRelayClientCredentials({
gatewayId, gatewayId,
gatewayName,
orgId, orgId,
orgName, orgName,
relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate, relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate,
@@ -895,11 +927,13 @@ export const relayServiceFactory = ({
host, host,
name, name,
identityId, identityId,
actorAuthMethod,
orgId orgId
}: { }: {
host: string; host: string;
name: string; name: string;
identityId?: string; identityId?: string;
actorAuthMethod?: ActorAuthMethod;
orgId?: string; orgId?: string;
}) => { }) => {
let relay: TRelays; let relay: TRelays;
@@ -908,6 +942,27 @@ export const relayServiceFactory = ({
await verifyHostInputValidity(host); await verifyHostInputValidity(host);
if (isOrgRelay) { if (isOrgRelay) {
const orgLicensePlan = await licenseService.getPlan(orgId);
if (!orgLicensePlan.gateway) {
throw new BadRequestError({
message:
"Relay registration failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
});
}
const { permission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityId,
orgId,
actorAuthMethod!,
orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionRelayActions.CreateRelays,
OrgPermissionSubjects.Relay
);
relay = await relayDAL.transaction(async (tx) => { relay = await relayDAL.transaction(async (tx) => {
const existingRelay = await relayDAL.findOne( const existingRelay = await relayDAL.findOne(
{ {
@@ -995,9 +1050,75 @@ export const relayServiceFactory = ({
}); });
}; };
const getRelays = async ({
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
const instanceRelays = await relayDAL.find({
orgId: null
});
const orgRelays = await relayDAL.find({
orgId: actorOrgId
});
return [...instanceRelays, ...orgRelays];
};
const deleteRelay = async ({
id,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
id: string;
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
const relay = await relayDAL.findById(id);
if (!relay || relay.orgId !== actorOrgId || relay.orgId === null) {
throw new NotFoundError({ message: "Relay not found" });
}
const deletedRelay = await relayDAL.deleteById(id);
return deletedRelay;
};
return { return {
registerRelay, registerRelay,
getCredentialsForGateway, getCredentialsForGateway,
getCredentialsForClient getCredentialsForClient,
getRelays,
deleteRelay
}; };
}; };
@@ -1,5 +1,7 @@
import { TSecretApprovalRequests } from "@app/db/schemas"; import { TSecretApprovalRequests } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
@@ -11,6 +13,7 @@ type TSendApprovalEmails = {
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
projectId: string; projectId: string;
secretApprovalRequest: TSecretApprovalRequests; secretApprovalRequest: TSecretApprovalRequests;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
}; };
export const sendApprovalEmailsFn = async ({ export const sendApprovalEmailsFn = async ({
@@ -18,7 +21,8 @@ export const sendApprovalEmailsFn = async ({
projectDAL, projectDAL,
smtpService, smtpService,
projectId, projectId,
secretApprovalRequest secretApprovalRequest,
notificationService
}: TSendApprovalEmails) => { }: TSendApprovalEmails) => {
const cfg = getConfig(); const cfg = getConfig();
@@ -26,6 +30,17 @@ export const sendApprovalEmailsFn = async ({
const project = await projectDAL.findProjectWithOrg(projectId); const project = await projectDAL.findProjectWithOrg(projectId);
await notificationService.createUserNotifications(
policy.userApprovers.map((approver) => ({
userId: approver.userId,
orgId: project.orgId,
type: NotificationType.SECRET_CHANGE_REQUEST,
title: "Secret Change Request",
body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`,
link: `/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}`
}))
);
// now we need to go through each of the reviewers and print out all the commits that they need to approve // now we need to go through each of the reviewers and print out all the commits that they need to approve
for await (const reviewerUser of policy.userApprovers) { for await (const reviewerUser of policy.userApprovers) {
await smtpService.sendMail({ await smtpService.sendMail({
@@ -28,6 +28,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -140,6 +142,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">; projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">; microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">; folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
}; };
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>; export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -172,7 +175,8 @@ export const secretApprovalRequestServiceFactory = ({
resourceMetadataDAL, resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
microsoftTeamsService, microsoftTeamsService,
folderCommitService folderCommitService,
notificationService
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ const requestCount = async ({
projectId, projectId,
@@ -1035,6 +1039,17 @@ export const secretApprovalRequestServiceFactory = ({
} }
}); });
await notificationService.createUserNotifications(
approverUsers.map((approver) => ({
userId: approver.id,
orgId: project.orgId,
type: NotificationType.SECRET_CHANGE_POLICY_BYPASSED,
title: "Secret Change Policy Bypassed",
body: `**${requestedByUser.firstName} ${requestedByUser.lastName}** (${requestedByUser.email}) has merged a secret to **${policy.secretPath}** in the **${env.name}** environment for project **${project.name}** without obtaining the required approval.`,
link: `/projects/secret-management/${project.id}/approval`
}))
);
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: "Infisical Secret Change Policy Bypassed", subjectLine: "Infisical Secret Change Policy Bypassed",
@@ -1069,7 +1084,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -1085,7 +1102,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
} }
}); });
} }
@@ -1104,7 +1123,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -1120,7 +1141,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
} }
}); });
} }
@@ -1446,7 +1469,8 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
secretApprovalRequest, secretApprovalRequest,
smtpService, smtpService,
projectId projectId,
notificationService
}); });
return secretApprovalRequest; return secretApprovalRequest;
@@ -1813,7 +1837,8 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
secretApprovalRequest, secretApprovalRequest,
smtpService, smtpService,
projectId projectId,
notificationService
}); });
return secretApprovalRequest; return secretApprovalRequest;
}; };
@@ -175,7 +175,8 @@ export const ldapPasswordRotationFactory: TRotationFactory<
const encryptedCredentials = await encryptAppConnectionCredentials({ const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials, credentials: updatedCredentials,
orgId, orgId,
kmsService kmsService,
projectId: connection.projectId
}); });
await appConnectionDAL.updateById(connection.id, { encryptedCredentials }); await appConnectionDAL.updateById(connection.id, { encryptedCredentials });
@@ -52,6 +52,7 @@ const baseSecretRotationV2Query = ({
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
db.ref("projectId").withSchema(TableName.AppConnection).as("connectionProjectId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db db
@@ -106,6 +107,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
connectionUpdatedAt, connectionUpdatedAt,
connectionVersion, connectionVersion,
connectionGatewayId, connectionGatewayId,
connectionProjectId,
connectionIsPlatformManagedCredentials, connectionIsPlatformManagedCredentials,
...el ...el
} = secretRotation; } = secretRotation;
@@ -126,6 +128,7 @@ const expandSecretRotation = <T extends Awaited<ReturnType<typeof baseSecretRota
updatedAt: connectionUpdatedAt, updatedAt: connectionUpdatedAt,
version: connectionVersion, version: connectionVersion,
gatewayId: connectionGatewayId, gatewayId: connectionGatewayId,
projectId: connectionProjectId,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
}, },
folder: { folder: {
@@ -17,6 +17,8 @@ import {
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
@@ -28,6 +30,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
}; };
export const secretRotationV2QueueServiceFactory = async ({ export const secretRotationV2QueueServiceFactory = async ({
@@ -36,7 +39,8 @@ export const secretRotationV2QueueServiceFactory = async ({
secretRotationV2Service, secretRotationV2Service,
projectMembershipDAL, projectMembershipDAL,
projectDAL, projectDAL,
smtpService smtpService,
notificationService
}: TSecretRotationV2QueueServiceFactoryDep) => { }: TSecretRotationV2QueueServiceFactoryDep) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -152,6 +156,19 @@ export const secretRotationV2QueueServiceFactory = async ({
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation]; const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`;
await notificationService.createUserNotifications(
projectAdmins.map((admin) => ({
userId: admin.userId,
orgId: project.orgId,
type: NotificationType.SECRET_ROTATION_FAILED,
title: "Secret Rotation Failed",
body: `Your **${rotationType}** rotation **${rotationName}** failed to rotate.`,
link: rotationPath
}))
);
await smtpService.sendMail({ await smtpService.sendMail({
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
template: SmtpTemplates.SecretRotationFailed, template: SmtpTemplates.SecretRotationFailed,
@@ -165,9 +182,7 @@ export const secretRotationV2QueueServiceFactory = async ({
secretPath: folder.path, secretPath: folder.path,
environment: environment.name, environment: environment.name,
projectName: project.name, projectName: project.name,
rotationUrl: encodeURI( rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`)
`${appCfg.SITE_URL}/projects/secret-management/${projectId}/secrets/${environment.slug}`
)
} }
}); });
} catch (error) { } catch (error) {
@@ -89,7 +89,7 @@ import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = { export type TSecretRotationV2ServiceFactoryDep = {
secretRotationV2DAL: TSecretRotationV2DALFactory; secretRotationV2DAL: TSecretRotationV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -459,7 +459,11 @@ export const secretRotationV2ServiceFactory = ({
const typeApp = SECRET_ROTATION_CONNECTION_MAP[payload.type]; const typeApp = SECRET_ROTATION_CONNECTION_MAP[payload.type];
// validates permission to connect and app is valid for rotation type // validates permission to connect and app is valid for rotation type
const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor); const connection = await appConnectionService.validateAppConnectionUsageById(
typeApp,
{ connectionId: payload.connectionId, projectId },
actor
);
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]( const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type](
{ {
@@ -431,7 +431,7 @@ export const secretRotationQueueFactory = ({
numberOfSecrets: numberOfSecretsRotated, numberOfSecrets: numberOfSecretsRotated,
environment: secretRotation.environment.slug, environment: secretRotation.environment.slug,
secretPath: secretRotation.secretPath, secretPath: secretRotation.secretPath,
workspaceId: secretRotation.projectId projectId: secretRotation.projectId
} }
}); });
@@ -50,6 +50,7 @@ const baseSecretScanningDataSourceQuery = ({
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
db.ref("projectId").withSchema(TableName.AppConnection).as("connectionProjectId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db db
@@ -84,6 +85,7 @@ const expandSecretScanningDataSource = <
connectionVersion, connectionVersion,
connectionIsPlatformManagedCredentials, connectionIsPlatformManagedCredentials,
connectionGatewayId, connectionGatewayId,
connectionProjectId,
...el ...el
} = dataSource; } = dataSource;
@@ -103,7 +105,8 @@ const expandSecretScanningDataSource = <
updatedAt: connectionUpdatedAt, updatedAt: connectionUpdatedAt,
version: connectionVersion, version: connectionVersion,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
gatewayId: connectionGatewayId gatewayId: connectionGatewayId,
projectId: connectionProjectId
} }
: undefined : undefined
}; };
@@ -21,6 +21,8 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
import { TAppConnection } from "@app/services/app-connection/app-connection-types"; import { TAppConnection } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
@@ -52,6 +54,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
}; };
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>; export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
@@ -65,7 +68,8 @@ export const secretScanningV2QueueServiceFactory = async ({
kmsService, kmsService,
auditLogService, auditLogService,
keyStore, keyStore,
appConnectionDAL appConnectionDAL,
notificationService
}: TSecretRotationV2QueueServiceFactoryDep) => { }: TSecretRotationV2QueueServiceFactoryDep) => {
const queueDataSourceFullScan = async ( const queueDataSourceFullScan = async (
dataSource: TSecretScanningDataSourceWithConnection, dataSource: TSecretScanningDataSourceWithConnection,
@@ -592,16 +596,38 @@ export const secretScanningV2QueueServiceFactory = async ({
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const subjectLine =
payload.status === SecretScanningScanStatus.Completed
? "Incident Alert: Secret(s) Leaked"
: `Secret Scanning Failed`;
await notificationService.createUserNotifications(
recipients.map((member) => ({
userId: member.userId,
orgId: project.orgId,
type:
payload.status === SecretScanningScanStatus.Completed
? NotificationType.SECRET_SCANNING_SECRETS_DETECTED
: NotificationType.SECRET_SCANNING_SCAN_FAILED,
title: subjectLine,
body:
payload.status === SecretScanningScanStatus.Completed
? `Uncovered **${payload.numberOfSecrets}** secret(s) ${payload.isDiffScan ? " from a recent commit to" : " in"} **${resourceName}**.`
: `Encountered an error while attempting to scan the resource **${resourceName}**: ${payload.errorMessage}`,
link:
payload.status === SecretScanningScanStatus.Completed
? `/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
: `/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
}))
);
await smtpService.sendMail({ await smtpService.sendMail({
recipients: recipients.map((member) => member.user.email!).filter(Boolean), recipients: recipients.map((member) => member.user.email!).filter(Boolean),
template: template:
payload.status === SecretScanningScanStatus.Completed payload.status === SecretScanningScanStatus.Completed
? SmtpTemplates.SecretScanningV2SecretsDetected ? SmtpTemplates.SecretScanningV2SecretsDetected
: SmtpTemplates.SecretScanningV2ScanFailed, : SmtpTemplates.SecretScanningV2ScanFailed,
subjectLine: subjectLine,
payload.status === SecretScanningScanStatus.Completed
? "Incident Alert: Secret(s) Leaked"
: `Secret Scanning Failed`,
substitutions: substitutions:
payload.status === SecretScanningScanStatus.Completed payload.status === SecretScanningScanStatus.Completed
? { ? {
@@ -60,7 +60,7 @@ import { TSecretScanningV2QueueServiceFactory } from "./secret-scanning-v2-queue
export type TSecretScanningV2ServiceFactoryDep = { export type TSecretScanningV2ServiceFactoryDep = {
secretScanningV2DAL: TSecretScanningV2DALFactory; secretScanningV2DAL: TSecretScanningV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -252,9 +252,9 @@ export const secretScanningV2ServiceFactory = ({
let connection: TAppConnection | null = null; let connection: TAppConnection | null = null;
if (payload.connectionId) { if (payload.connectionId) {
// validates permission to connect and app is valid for data source // validates permission to connect and app is valid for data source
connection = await appConnectionService.connectAppConnectionById( connection = await appConnectionService.validateAppConnectionUsageById(
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[payload.type], SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[payload.type],
payload.connectionId, { connectionId: payload.connectionId, projectId: payload.projectId },
actor actor
); );
} }
@@ -373,9 +373,9 @@ export const secretScanningV2ServiceFactory = ({
let connection: TAppConnection | null = null; let connection: TAppConnection | null = null;
if (dataSource.connectionId) { if (dataSource.connectionId) {
// validates permission to connect and app is valid for data source // validates permission to connect and app is valid for data source
connection = await appConnectionService.connectAppConnectionById( connection = await appConnectionService.validateAppConnectionUsageById(
SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSource.type], SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSource.type],
dataSource.connectionId, { connectionId: dataSource.connectionId, projectId: dataSource.projectId },
actor actor
); );
} }
+50 -32
View File
@@ -242,7 +242,12 @@ export const LDAP_AUTH = {
accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.", accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
lockoutEnabled: "Whether the lockout feature is enabled.",
lockoutThreshold: "The amount of times login must fail before locking the identity auth method.",
lockoutDurationSeconds: "How long an identity auth method lockout lasts.",
lockoutCounterResetSeconds:
"How long to wait from the most recent failed login until resetting the lockout counter."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the configuration for.", identityId: "The ID of the identity to update the configuration for.",
@@ -257,13 +262,21 @@ export const LDAP_AUTH = {
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
templateId: "The ID of the identity auth template to update the configuration to." templateId: "The ID of the identity auth template to update the configuration to.",
lockoutEnabled: "Whether the lockout feature is enabled.",
lockoutThreshold: "The amount of times login must fail before locking the identity auth method.",
lockoutDurationSeconds: "How long an identity auth method lockout lasts.",
lockoutCounterResetSeconds:
"How long to wait from the most recent failed login until resetting the lockout counter."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the configuration for." identityId: "The ID of the identity to retrieve the configuration for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the configuration for." identityId: "The ID of the identity to revoke the configuration for."
},
CLEAR_CLIENT_LOCKOUTS: {
identityId: "The ID of the identity to clear the client lockouts from."
} }
} as const; } as const;
@@ -711,13 +724,13 @@ export const PROJECTS = {
template: "The name of the project template, if specified, to apply to this project." template: "The name of the project template, if specified, to apply to this project."
}, },
DELETE: { DELETE: {
workspaceId: "The ID of the project to delete." projectId: "The ID of the project to delete."
}, },
GET: { GET: {
workspaceId: "The ID of the project." projectId: "The ID of the project."
}, },
UPDATE: { UPDATE: {
workspaceId: "The ID of the project to update.", projectId: "The ID of the project to update.",
name: "The new name of the project.", name: "The new name of the project.",
projectDescription: "An optional description label for the project.", projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project.",
@@ -729,10 +742,10 @@ export const PROJECTS = {
secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection." secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection."
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." projectId: "The ID of the project to get the key from."
}, },
GET_SNAPSHOTS: { GET_SNAPSHOTS: {
workspaceId: "The ID of the project to get snapshots from.", projectId: "The ID of the project to get snapshots from.",
environment: "The environment to get snapshots from.", environment: "The environment to get snapshots from.",
path: "The secret path to get snapshots from.", path: "The secret path to get snapshots from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th snapshot.", offset: "The offset to start from. If you enter 10, it will start from the 10th snapshot.",
@@ -759,10 +772,10 @@ export const PROJECTS = {
projectId: "The ID of the project to list groups for." projectId: "The ID of the project to list groups for."
}, },
LIST_INTEGRATION: { LIST_INTEGRATION: {
workspaceId: "The ID of the project to list integrations for." projectId: "The ID of the project to list integrations for."
}, },
LIST_INTEGRATION_AUTHORIZATION: { LIST_INTEGRATION_AUTHORIZATION: {
workspaceId: "The ID of the project to list integration auths for." projectId: "The ID of the project to list integration auths for."
}, },
LIST_SSH_CAS: { LIST_SSH_CAS: {
projectId: "The ID of the project to list SSH CAs for." projectId: "The ID of the project to list SSH CAs for."
@@ -815,15 +828,15 @@ export const PROJECT_USERS = {
usernames: "A list of usernames to remove from the project." usernames: "A list of usernames to remove from the project."
}, },
GET_USER_MEMBERSHIPS: { GET_USER_MEMBERSHIPS: {
workspaceId: "The ID of the project to get memberships from." projectId: "The ID of the project to get memberships from."
}, },
GET_USER_MEMBERSHIP: { GET_USER_MEMBERSHIP: {
workspaceId: "The ID of the project to get memberships from.", projectId: "The ID of the project to get memberships from.",
membershipId: "The ID of the user's project membership.", membershipId: "The ID of the user's project membership.",
username: "The username to get project membership of. Email is the default username." username: "The username to get project membership of. Email is the default username."
}, },
UPDATE_USER_MEMBERSHIP: { UPDATE_USER_MEMBERSHIP: {
workspaceId: "The ID of the project to update the membership for.", projectId: "The ID of the project to update the membership for.",
membershipId: "The ID of the membership to update.", membershipId: "The ID of the membership to update.",
roles: "A list of roles to update the membership to." roles: "A list of roles to update the membership to."
} }
@@ -877,31 +890,31 @@ export const PROJECT_IDENTITIES = {
export const ENVIRONMENTS = { export const ENVIRONMENTS = {
CREATE: { CREATE: {
workspaceId: "The ID of the project to create the environment in.", projectId: "The ID of the project to create the environment in.",
name: "The name of the environment to create.", name: "The name of the environment to create.",
slug: "The slug of the environment to create.", slug: "The slug of the environment to create.",
position: "The position of the environment. The lowest number will be displayed as the first environment." position: "The position of the environment. The lowest number will be displayed as the first environment."
}, },
UPDATE: { UPDATE: {
workspaceId: "The ID of the project to update the environment in.", projectId: "The ID of the project to update the environment in.",
id: "The ID of the environment to update.", id: "The ID of the environment to update.",
name: "The new name of the environment.", name: "The new name of the environment.",
slug: "The new slug of the environment.", slug: "The new slug of the environment.",
position: "The new position of the environment. The lowest number will be displayed as the first environment." position: "The new position of the environment. The lowest number will be displayed as the first environment."
}, },
DELETE: { DELETE: {
workspaceId: "The ID of the project to delete the environment from.", projectId: "The ID of the project to delete the environment from.",
id: "The ID of the environment to delete." id: "The ID of the environment to delete."
}, },
GET: { GET: {
workspaceId: "The ID of the project the environment belongs to.", projectId: "The ID of the project the environment belongs to.",
id: "The ID of the environment to fetch." id: "The ID of the environment to fetch."
} }
} as const; } as const;
export const FOLDERS = { export const FOLDERS = {
LIST: { LIST: {
workspaceId: "The ID of the project to list folders from.", projectId: "The ID of the project to list folders from.",
environment: "The slug of the environment to list folders from.", environment: "The slug of the environment to list folders from.",
path: "The path to list folders from.", path: "The path to list folders from.",
directory: "The directory to list folders from. (Deprecated in favor of path)", directory: "The directory to list folders from. (Deprecated in favor of path)",
@@ -913,7 +926,7 @@ export const FOLDERS = {
folderId: "The ID of the folder to get details." folderId: "The ID of the folder to get details."
}, },
CREATE: { CREATE: {
workspaceId: "The ID of the project to create the folder in.", projectId: "The ID of the project to create the folder in.",
environment: "The slug of the environment to create the folder in.", environment: "The slug of the environment to create the folder in.",
name: "The name of the folder to create.", name: "The name of the folder to create.",
path: "The path of the folder to create.", path: "The path of the folder to create.",
@@ -927,12 +940,12 @@ export const FOLDERS = {
path: "The path of the folder to update.", path: "The path of the folder to update.",
directory: "The new directory of the folder to update. (Deprecated in favor of path)", directory: "The new directory of the folder to update. (Deprecated in favor of path)",
projectSlug: "The slug of the project where the folder is located.", projectSlug: "The slug of the project where the folder is located.",
workspaceId: "The ID of the project where the folder is located.", projectId: "The ID of the project where the folder is located.",
description: "An optional description label for the folder." description: "An optional description label for the folder."
}, },
DELETE: { DELETE: {
folderIdOrName: "The ID or name of the folder to delete.", folderIdOrName: "The ID or name of the folder to delete.",
workspaceId: "The ID of the project to delete the folder from.", projectId: "The ID of the project to delete the folder from.",
environment: "The slug of the environment where the folder is located.", environment: "The slug of the environment where the folder is located.",
directory: "The directory of the folder to delete. (Deprecated in favor of path)", directory: "The directory of the folder to delete. (Deprecated in favor of path)",
path: "The path of the folder to delete." path: "The path of the folder to delete."
@@ -964,7 +977,7 @@ export const RAW_SECRETS = {
expand: "Whether or not to expand secret references.", expand: "Whether or not to expand secret references.",
recursive: recursive:
"Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.", "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
workspaceId: "The ID of the project to list secrets from.", projectId: "The ID of the project to list secrets from.",
workspaceSlug: workspaceSlug:
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.", "The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
@@ -984,7 +997,7 @@ export const RAW_SECRETS = {
secretValue: "The value of the secret to create.", secretValue: "The value of the secret to create.",
skipMultilineEncoding: "Skip multiline encoding for the secret value.", skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to create.", type: "The type of the secret to create.",
workspaceId: "The ID of the project to create the secret in.", projectId: "The ID of the project to create the secret in.",
tagIds: "The ID of the tags to be attached to the created secret.", tagIds: "The ID of the tags to be attached to the created secret.",
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.", secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.",
secretReminderNote: "Note to be attached in notification email." secretReminderNote: "Note to be attached in notification email."
@@ -992,7 +1005,7 @@ export const RAW_SECRETS = {
GET: { GET: {
expand: "Whether or not to expand secret references.", expand: "Whether or not to expand secret references.",
secretName: "The name of the secret to get.", secretName: "The name of the secret to get.",
workspaceId: "The ID of the project to get the secret from.", projectId: "The ID of the project to get the secret from.",
workspaceSlug: "The slug of the project to get the secret from.", workspaceSlug: "The slug of the project to get the secret from.",
environment: "The slug of the environment to get the secret from.", environment: "The slug of the environment to get the secret from.",
secretPath: "The path of the secret to get.", secretPath: "The path of the secret to get.",
@@ -1011,7 +1024,7 @@ export const RAW_SECRETS = {
skipMultilineEncoding: "Skip multiline encoding for the secret value.", skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to update.", type: "The type of the secret to update.",
projectSlug: "The slug of the project to update the secret in.", projectSlug: "The slug of the project to update the secret in.",
workspaceId: "The ID of the project to update the secret in.", projectId: "The ID of the project to update the secret in.",
tagIds: "The ID of the tags to be attached to the updated secret.", tagIds: "The ID of the tags to be attached to the updated secret.",
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.", secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.",
secretReminderNote: "Note to be attached in notification email.", secretReminderNote: "Note to be attached in notification email.",
@@ -1025,11 +1038,11 @@ export const RAW_SECRETS = {
secretPath: "The path of the secret.", secretPath: "The path of the secret.",
type: "The type of the secret to delete.", type: "The type of the secret to delete.",
projectSlug: "The slug of the project to delete the secret in.", projectSlug: "The slug of the project to delete the secret in.",
workspaceId: "The ID of the project where the secret is located." projectId: "The ID of the project where the secret is located."
}, },
GET_REFERENCE_TREE: { GET_REFERENCE_TREE: {
secretName: "The name of the secret to get the reference tree for.", secretName: "The name of the secret to get the reference tree for.",
workspaceId: "The ID of the project where the secret is located.", projectId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.", environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located." secretPath: "The folder path where the secret is located."
}, },
@@ -1043,7 +1056,7 @@ export const RAW_SECRETS = {
export const SECRET_IMPORTS = { export const SECRET_IMPORTS = {
LIST: { LIST: {
workspaceId: "The ID of the project to list secret imports from.", projectId: "The ID of the project to list secret imports from.",
environment: "The slug of the environment to list secret imports from.", environment: "The slug of the environment to list secret imports from.",
path: "The path to list secret imports from." path: "The path to list secret imports from."
}, },
@@ -1053,7 +1066,7 @@ export const SECRET_IMPORTS = {
CREATE: { CREATE: {
environment: "The slug of the environment to import into.", environment: "The slug of the environment to import into.",
path: "The path to import into.", path: "The path to import into.",
workspaceId: "The ID of the project you are working in.", projectId: "The ID of the project you are working in.",
isReplication: isReplication:
"When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.", "When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.",
import: { import: {
@@ -1070,10 +1083,10 @@ export const SECRET_IMPORTS = {
position: "The new position of the secret import. The lowest number will be displayed as the first import." position: "The new position of the secret import. The lowest number will be displayed as the first import."
}, },
path: "The path of the secret import to update.", path: "The path of the secret import to update.",
workspaceId: "The ID of the project where the secret import is located." projectId: "The ID of the project where the secret import is located."
}, },
DELETE: { DELETE: {
workspaceId: "The ID of the project to delete the secret import from.", projectId: "The ID of the project to delete the secret import from.",
secretImportId: "The ID of the secret import to delete.", secretImportId: "The ID of the secret import to delete.",
environment: "The slug of the environment where the secret import is located.", environment: "The slug of the environment where the secret import is located.",
path: "The path of the secret import to delete." path: "The path of the secret import to delete."
@@ -2185,11 +2198,15 @@ export const CertificateAuthorities = {
}; };
export const AppConnections = { export const AppConnections = {
LIST: (app?: AppConnection) => ({
projectId: `The ID of the project to list ${app ? APP_CONNECTION_NAME_MAP[app] : "App"} Connections from.`
}),
GET_BY_ID: (app: AppConnection) => ({ GET_BY_ID: (app: AppConnection) => ({
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.` connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
}), }),
GET_BY_NAME: (app: AppConnection) => ({ GET_BY_NAME: (app: AppConnection) => ({
connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.` connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`,
projectId: `The project ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection is associated with. Leave unspecified to get organization-level connections.`
}), }),
CREATE: (app: AppConnection) => { CREATE: (app: AppConnection) => {
const appName = APP_CONNECTION_NAME_MAP[app]; const appName = APP_CONNECTION_NAME_MAP[app];
@@ -2198,7 +2215,8 @@ export const AppConnections = {
description: `An optional description for the ${appName} Connection.`, description: `An optional description for the ${appName} Connection.`,
credentials: `The credentials used to connect with ${appName}.`, credentials: `The credentials used to connect with ${appName}.`,
method: `The method used to authenticate with ${appName}.`, method: `The method used to authenticate with ${appName}.`,
isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.` isPlatformManagedCredentials: `Whether or not the ${appName} Connection credentials should be managed by Infisical. Once enabled this cannot be reversed.`,
projectId: `The ID of the project to create the ${appName} Connection in.`
}; };
}, },
UPDATE: (app: AppConnection) => { UPDATE: (app: AppConnection) => {
+2
View File
@@ -231,6 +231,8 @@ export type TQueueJobTypes = {
[QueueName.ImportSecretsFromExternalSource]: { [QueueName.ImportSecretsFromExternalSource]: {
name: QueueJobs.ImportSecretsFromExternalSource; name: QueueJobs.ImportSecretsFromExternalSource;
payload: { payload: {
orgId: string;
actorId: string;
actorEmail: string; actorEmail: string;
importType: ExternalPlatforms; importType: ExternalPlatforms;
data: { data: {
+27 -12
View File
@@ -328,6 +328,7 @@ import { registerV1Routes } from "./v1";
import { initializeOauthConfigSync } from "./v1/sso-router"; import { initializeOauthConfigSync } from "./v1/sso-router";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3"; import { registerV3Routes } from "./v3";
import { registerV4Routes } from "./v4";
const histogram = monitorEventLoopDelay({ resolution: 20 }); const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable(); histogram.enable();
@@ -772,7 +773,8 @@ export const registerRoutes = async (
orgDAL, orgDAL,
totpService, totpService,
orgMembershipDAL, orgMembershipDAL,
auditLogService auditLogService,
notificationService
}); });
const passwordService = authPaswordServiceFactory({ const passwordService = authPaswordServiceFactory({
tokenService, tokenService,
@@ -886,7 +888,8 @@ export const registerRoutes = async (
projectDAL, projectDAL,
permissionService, permissionService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
projectMembershipDAL projectMembershipDAL,
notificationService
}); });
const rateLimitService = rateLimitServiceFactory({ const rateLimitService = rateLimitServiceFactory({
@@ -925,7 +928,8 @@ export const registerRoutes = async (
projectRoleDAL, projectRoleDAL,
groupProjectDAL, groupProjectDAL,
secretReminderRecipientsDAL, secretReminderRecipientsDAL,
licenseService licenseService,
notificationService
}); });
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({ const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
permissionService, permissionService,
@@ -1106,7 +1110,9 @@ export const registerRoutes = async (
instanceRelayConfigDAL, instanceRelayConfigDAL,
orgRelayConfigDAL, orgRelayConfigDAL,
relayDAL, relayDAL,
kmsService kmsService,
licenseService,
permissionService
}); });
const gatewayV2Service = gatewayV2ServiceFactory({ const gatewayV2Service = gatewayV2ServiceFactory({
@@ -1144,7 +1150,8 @@ export const registerRoutes = async (
appConnectionDAL, appConnectionDAL,
licenseService, licenseService,
gatewayService, gatewayService,
gatewayV2Service gatewayV2Service,
notificationService
}); });
const secretQueueService = secretQueueFactory({ const secretQueueService = secretQueueFactory({
@@ -1228,7 +1235,8 @@ export const registerRoutes = async (
projectTemplateService, projectTemplateService,
groupProjectDAL, groupProjectDAL,
smtpService, smtpService,
reminderService reminderService,
notificationService
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -1362,7 +1370,8 @@ export const registerRoutes = async (
resourceMetadataDAL, resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
microsoftTeamsService, microsoftTeamsService,
folderCommitService folderCommitService,
notificationService
}); });
const secretService = secretServiceFactory({ const secretService = secretServiceFactory({
@@ -1680,7 +1689,8 @@ export const registerRoutes = async (
identityOrgMembershipDAL, identityOrgMembershipDAL,
licenseService, licenseService,
identityDAL, identityDAL,
identityAuthTemplateDAL identityAuthTemplateDAL,
keyStore
}); });
const dynamicSecretProviders = buildDynamicSecretProviders({ const dynamicSecretProviders = buildDynamicSecretProviders({
@@ -1812,7 +1822,8 @@ export const registerRoutes = async (
secretV2BridgeService, secretV2BridgeService,
resourceMetadataDAL, resourceMetadataDAL,
folderCommitService, folderCommitService,
folderVersionDAL folderVersionDAL,
notificationService
}); });
const migrationService = externalMigrationServiceFactory({ const migrationService = externalMigrationServiceFactory({
@@ -1837,7 +1848,8 @@ export const registerRoutes = async (
gatewayService, gatewayService,
gatewayV2Service, gatewayV2Service,
gatewayDAL, gatewayDAL,
gatewayV2DAL gatewayV2DAL,
projectDAL
}); });
const secretSyncService = secretSyncServiceFactory({ const secretSyncService = secretSyncServiceFactory({
@@ -2014,7 +2026,8 @@ export const registerRoutes = async (
queueService, queueService,
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
smtpService smtpService,
notificationService
}); });
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({ const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
@@ -2026,7 +2039,8 @@ export const registerRoutes = async (
smtpService, smtpService,
kmsService, kmsService,
keyStore, keyStore,
appConnectionDAL appConnectionDAL,
notificationService
}); });
const secretScanningV2Service = secretScanningV2ServiceFactory({ const secretScanningV2Service = secretScanningV2ServiceFactory({
@@ -2294,6 +2308,7 @@ export const registerRoutes = async (
{ prefix: "/api/v2" } { prefix: "/api/v2" }
); );
await server.register(registerV3Routes, { prefix: "/api/v3" }); await server.register(registerV3Routes, { prefix: "/api/v3" });
await server.register(registerV4Routes, { prefix: "/api/v4" });
server.addHook("onClose", async () => { server.addHook("onClose", async () => {
cronJobs.forEach((job) => job.stop()); cronJobs.forEach((job) => job.stop());
@@ -26,6 +26,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
description?: string | null; description?: string | null;
isPlatformManagedCredentials?: boolean; isPlatformManagedCredentials?: boolean;
gatewayId?: string | null; gatewayId?: string | null;
projectId?: string;
}>; }>;
updateSchema: z.ZodType<{ updateSchema: z.ZodType<{
name?: string; name?: string;
@@ -47,18 +48,27 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], tags: [ApiDocsTags.AppConnections],
description: `List the ${appName} Connections for the current organization.`, description: `List the ${appName} Connections for the current organization or project.`,
querystring: z.object({
projectId: z.string().optional().describe(AppConnections.LIST(app).projectId)
}),
response: { response: {
200: z.object({ appConnections: sanitizedResponseSchema.array() }) 200: z.object({ appConnections: sanitizedResponseSchema.array() })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[]; const { projectId } = req.query;
const appConnections = (await server.services.appConnection.listAppConnections(
req.permission,
app,
projectId
)) as T[];
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.GET_APP_CONNECTIONS, type: EventType.GET_APP_CONNECTIONS,
metadata: { metadata: {
@@ -82,14 +92,19 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], tags: [ApiDocsTags.AppConnections],
description: `List the ${appName} Connections the current user has permission to establish connections with.`, description: `List the ${appName} Connections the current user has permission to establish connections within this project.`,
querystring: z.object({
projectId: z.string().optional().describe(AppConnections.LIST(app).projectId)
}),
response: { response: {
200: z.object({ 200: z.object({
appConnections: z appConnections: z
.object({ .object({
app: z.literal(app), app: z.literal(app),
name: z.string(), name: z.string(),
id: z.string().uuid() id: z.string().uuid(),
projectId: z.string().nullish(),
orgId: z.string()
}) })
.array() .array()
}) })
@@ -97,14 +112,17 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { projectId } = req.query;
const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser( const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser(
app, app,
req.permission req.permission,
projectId
); );
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS, type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS,
metadata: { metadata: {
@@ -149,6 +167,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.GET_APP_CONNECTION, type: EventType.GET_APP_CONNECTION,
metadata: { metadata: {
@@ -178,6 +197,9 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
.min(1, "Connection name required") .min(1, "Connection name required")
.describe(AppConnections.GET_BY_NAME(app).connectionName) .describe(AppConnections.GET_BY_NAME(app).connectionName)
}), }),
querystring: z.object({
projectId: z.string().trim().optional().describe(AppConnections.GET_BY_NAME(app).projectId)
}),
response: { response: {
200: z.object({ appConnection: sanitizedResponseSchema }) 200: z.object({ appConnection: sanitizedResponseSchema })
} }
@@ -185,16 +207,21 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { connectionName } = req.params; const { connectionName } = req.params;
const { projectId } = req.query;
const appConnection = (await server.services.appConnection.findAppConnectionByName( const appConnection = (await server.services.appConnection.findAppConnectionByName(
app, app,
connectionName, {
connectionName,
projectId
},
req.permission req.permission
)) as T; )) as T;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.GET_APP_CONNECTION, type: EventType.GET_APP_CONNECTION,
metadata: { metadata: {
@@ -216,9 +243,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], tags: [ApiDocsTags.AppConnections],
description: `Create ${ description: `Create ${startsWithVowel(appName) ? "an" : "a"} ${appName} Connection.`,
startsWithVowel(appName) ? "an" : "a"
} ${appName} Connection for the current organization.`,
body: createSchema, body: createSchema,
response: { response: {
200: z.object({ appConnection: sanitizedResponseSchema }) 200: z.object({ appConnection: sanitizedResponseSchema })
@@ -226,16 +251,17 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body; const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId, projectId } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection( const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId }, { name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId, projectId },
req.permission req.permission
)) as T; )) as T;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.CREATE_APP_CONNECTION, type: EventType.CREATE_APP_CONNECTION,
metadata: { metadata: {
@@ -283,6 +309,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.UPDATE_APP_CONNECTION, type: EventType.UPDATE_APP_CONNECTION,
metadata: { metadata: {
@@ -329,6 +356,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId: appConnection.projectId ?? undefined,
event: { event: {
type: EventType.DELETE_APP_CONNECTION, type: EventType.DELETE_APP_CONNECTION,
metadata: { metadata: {
@@ -340,4 +368,81 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
return { appConnection }; return { appConnection };
} }
}); });
// scott: we will need this once we have individual app connection page and may want to expose to API
// server.route({
// method: "GET",
// url: `/:connectionId/usage`,
// config: {
// rateLimit: readLimit
// },
// schema: {
// hide: true, // scott: we could expose this in the future but just for UI right now
// tags: [ApiDocsTags.AppConnections],
// params: z.object({
// connectionId: z.string().uuid()
// }),
// response: {
// 200: z.object({
// projects: z
// .object({
// id: z.string(),
// name: z.string(),
// type: z.nativeEnum(ProjectType),
// slug: z.string(),
// resources: z.object({
// secretSyncs: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array(),
// secretRotations: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array(),
// externalCas: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array(),
// dataSources: z
// .object({
// id: z.string(),
// name: z.string()
// })
// .array()
// })
// })
// .array()
// })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async (req) => {
// const { connectionId } = req.params;
//
// const projects = await server.services.appConnection.findAppConnectionUsageById(
// app,
// connectionId,
// req.permission
// );
//
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// orgId: req.permission.orgId,
// event: {
// type: EventType.GET_APP_CONNECTION_USAGE,
// metadata: {
// connectionId
// }
// }
// });
//
// return { projects };
// }
// });
}; };
@@ -1,12 +1,13 @@
import { z } from "zod"; import { z } from "zod";
import { ProjectType } from "@app/db/schemas";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci"; import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci";
import { import {
OracleDBConnectionListItemSchema, OracleDBConnectionListItemSchema,
SanitizedOracleDBConnectionSchema SanitizedOracleDBConnectionSchema
} from "@app/ee/services/app-connections/oracledb"; } from "@app/ee/services/app-connections/oracledb";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs"; import { ApiDocsTags, AppConnections } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { import {
@@ -210,6 +211,9 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], tags: [ApiDocsTags.AppConnections],
description: "List the available App Connection Options.", description: "List the available App Connection Options.",
querystring: z.object({
projectType: z.nativeEnum(ProjectType).optional()
}),
response: { response: {
200: z.object({ 200: z.object({
appConnectionOptions: AppConnectionOptionsSchema.array() appConnectionOptions: AppConnectionOptionsSchema.array()
@@ -217,8 +221,8 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => { handler: (req) => {
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions(); const appConnectionOptions = server.services.appConnection.listAppConnectionOptions(req.query.projectType);
return { appConnectionOptions }; return { appConnectionOptions };
} }
}); });
@@ -232,18 +236,27 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AppConnections], tags: [ApiDocsTags.AppConnections],
description: "List all the App Connections for the current organization.", description: "List all the App Connections for the current organization or project.",
querystring: z.object({
projectId: z.string().optional().describe(AppConnections.LIST().projectId)
}),
response: { response: {
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() }) 200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission); const { projectId } = req.query;
const appConnections = await server.services.appConnection.listAppConnections(
req.permission,
undefined,
projectId
);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: req.permission.orgId, orgId: req.permission.orgId,
projectId,
event: { event: {
type: EventType.GET_APP_CONNECTIONS, type: EventType.GET_APP_CONNECTIONS,
metadata: { metadata: {
+364 -62
View File
@@ -1,16 +1,16 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas"; import { SecretFoldersSchema, SecretImportsSchema, SecretType, UsersSchema } from "@app/db/schemas";
import { RemindersSchema } from "@app/db/schemas/reminders"; import { RemindersSchema } from "@app/db/schemas/reminders";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { DASHBOARD } from "@app/lib/api-docs"; import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { secretsLimit } from "@app/server/config/rateLimiter"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -111,6 +111,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
SecretRotationV2Schema, SecretRotationV2Schema,
z.object({ z.object({
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
@@ -124,7 +125,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array() .array()
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
isEmpty: z.boolean(),
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
@@ -207,7 +210,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
const environments = req.query.environments.split(","); const environments = req.query.environments.split(",");
if (!projectId || environments.length === 0) if (!projectId || environments.length === 0)
throw new BadRequestError({ message: "Missing workspace id or environment(s)" }); throw new BadRequestError({ message: "Missing project id or environment(s)" });
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId); const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
@@ -219,7 +222,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined; let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined; let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined; let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>>[number] & { isEmpty: boolean })[]
| undefined;
let dynamicSecrets: let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>> | Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
| undefined; | undefined;
@@ -426,43 +431,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = await server.services.secret.getSecretsRawMultiEnv({ secrets = (
viewSecretValue: true, await server.services.secret.getSecretsRawMultiEnv({
actorId: req.permission.id, viewSecretValue: true,
actor: req.permission.type, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actor: req.permission.type,
environments, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, environments,
projectId, actorAuthMethod: req.permission.authMethod,
path: secretPath, projectId,
orderBy, path: secretPath,
orderDirection, orderBy,
search, orderDirection,
limit: remainingLimit, search,
offset: adjustedOffset, limit: remainingLimit,
isInternal: true offset: adjustedOffset,
}); isInternal: true
})
).map((secret) => ({ ...secret, isEmpty: !secret.secretValue }));
} }
} }
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
for await (const environment of environments) { for await (const environment of environments) {
const secretCountFromEnv = const secretIds = [
(secrets?.filter((secret) => secret.environment === environment).length ?? 0) + ...new Set(
(secretRotations [
?.filter((rotation) => rotation.environment.slug === environment) ...(secrets?.filter((secret) => secret.environment === environment) ?? []),
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); ...(secretRotations
?.filter((rotation) => rotation.environment.slug === environment)
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
].map((secret) => secret.id)
)
];
if (secretCountFromEnv) { if (secretIds) {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secretCountFromEnv numberOfSecrets: secretIds.length,
secretIds
} }
} }
}); });
@@ -473,8 +486,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secretCountFromEnv, numberOfSecrets: secretIds.length,
workspaceId: projectId, projectId,
environment, environment,
secretPath, secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -584,7 +597,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
viewSecretValue: booleanSchema.default(true),
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
@@ -606,7 +618,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
SecretRotationV2Schema, SecretRotationV2Schema,
z.object({ z.object({
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
isEmpty: z.boolean(),
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
@@ -619,7 +633,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array() .array()
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
isEmpty: z.boolean(),
secretReminderRecipients: z secretReminderRecipients: z
.object({ .object({
user: UsersSchema.pick({ id: true, email: true, username: true }), user: UsersSchema.pick({ id: true, email: true, username: true }),
@@ -696,7 +712,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeSecretRotations includeSecretRotations
} = req.query; } = req.query;
if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); if (!projectId || !environment) throw new BadRequestError({ message: "Missing project id or environment" });
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId); const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
@@ -715,12 +731,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined; let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
let secrets: let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & { | (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
isEmpty: boolean;
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null; reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
})[] })[]
| undefined; | undefined;
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined; let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let secretRotations: let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>> | (Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>[number] & {
secrets: (NonNullable<
Awaited<
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
>[number]["secrets"][number] & {
isEmpty: boolean;
}
> | null)[];
})[]
| undefined; | undefined;
let totalImportCount: number | undefined; let totalImportCount: number | undefined;
@@ -822,19 +847,31 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
); );
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations( secretRotations = (
{ await server.services.secretRotationV2.getDashboardSecretRotations(
projectId, {
search, projectId,
orderBy, search,
orderDirection, orderBy,
environments: [environment], orderDirection,
secretPath, environments: [environment],
limit: remainingLimit, secretPath,
offset: adjustedOffset limit: remainingLimit,
}, offset: adjustedOffset
req.permission },
); req.permission
)
).map((rotation) => ({
...rotation,
secrets: rotation.secrets.map((secret) =>
secret
? {
...secret,
isEmpty: !secret.secretValue
}
: secret
)
}));
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
@@ -919,7 +956,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
await server.services.secret.getSecretsRaw({ await server.services.secret.getSecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
viewSecretValue: req.query.viewSecretValue, viewSecretValue: true,
throwOnMissingReadValuePermission: false, throwOnMissingReadValuePermission: false,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environment, environment,
@@ -943,6 +980,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets = rawSecrets.map((secret) => ({ secrets = rawSecrets.map((secret) => ({
...secret, ...secret,
isEmpty: !secret.secretValue,
reminder: reminders[secret.id] ?? null reminder: reminders[secret.id] ?? null
})); }));
} }
@@ -977,19 +1015,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
})); }));
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
const secretCount = const secretIds = [
(secrets?.length ?? 0) + ...new Set(
(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); [
...(secrets ?? []),
...(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
].map((secret) => secret.id)
)
];
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secretCount numberOfSecrets: secretIds.length,
secretIds
} }
} }
}); });
@@ -1000,8 +1044,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secretCount, numberOfSecrets: secretIds.length,
workspaceId: projectId, projectId,
environment, environment,
secretPath, secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1060,6 +1104,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array() .array()
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
@@ -1145,18 +1190,20 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
); );
for await (const environment of environments) { for await (const environment of environments) {
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length; const envSecrets = secrets.filter((secret) => secret.environment === environment);
const secretCountForEnv = envSecrets.length;
if (secretCountForEnv) { if (secretCountForEnv) {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secretCountForEnv numberOfSecrets: secretCountForEnv,
secretIds: envSecrets.map((secret) => secret.id)
} }
} }
}); });
@@ -1168,7 +1215,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secretCountForEnv, numberOfSecrets: secretCountForEnv,
workspaceId: projectId, projectId,
environment, environment,
secretPath, secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1259,6 +1306,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
@@ -1310,6 +1358,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
@@ -1345,11 +1394,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secrets.length numberOfSecrets: secrets.length,
secretIds: secrets.map((secret) => secret.id)
} }
} }
}); });
@@ -1361,7 +1411,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: projectId, projectId,
environment, environment,
secretPath, secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1373,4 +1423,256 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
return { secrets }; return { secrets };
} }
}); });
server.route({
method: "GET",
url: "/secret-value",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
secretKey: z.string().trim(),
isOverride: z
.enum(["true", "false"])
.transform((value) => value === "true")
.optional()
}),
response: {
200: z.object({
valueOverride: z.string().optional(),
value: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, environment, secretKey, isOverride } = req.query;
// TODO (scott): just get the secret instead of searching for it in list
const { secrets } = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
viewSecretValue: true,
throwOnMissingReadValuePermission: false,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
search: secretKey,
includeTagsInSearch: true,
includeMetadataInSearch: true
});
if (isOverride) {
const personalSecret = secrets.find(
(secret) => secret.type === SecretType.Personal && secret.secretKey === secretKey
);
if (!personalSecret)
throw new BadRequestError({
message: `Could not find personal secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
if (personalSecret)
return {
valueOverride: personalSecret.secretValue
};
}
const sharedSecret = secrets.find(
(secret) => secret.type === SecretType.Shared && secret.secretKey === secretKey
);
if (!sharedSecret)
throw new BadRequestError({
message: `Could not find secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
// only audit if not personal
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_GET_SECRET_VALUE,
metadata: {
environment: req.query.environment,
secretPath: req.query.secretPath,
secretKey,
secretId: sharedSecret.id
}
}
});
return { value: sharedSecret.secretValue };
}
});
server.route({
url: "/secret-imports",
method: "GET",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
secrets: z
.object({
secretPath: z.string(),
environment: z.string(),
environmentInfo: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
folderId: z.string().optional(),
secrets: secretRawSchema.omit({ secretValue: true }).extend({ isEmpty: z.boolean() }).array()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
projectId: req.query.projectId,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_LIST_SECRETS,
metadata: {
environment: req.query.environment,
secretPath: req.query.path,
numberOfSecrets: importedSecrets.length,
secretIds: importedSecrets.map((secret) => secret.id)
}
}
});
return {
secrets: importedSecrets.map((importData) => ({
...importData,
secrets: importData.secrets.map((secret) => ({
...secret,
isEmpty: !secret.secretValue
}))
}))
};
}
});
server.route({
method: "GET",
url: "/secret-versions/:secretId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
secretId: z.string()
}),
querystring: z.object({
offset: z.coerce.number(),
limit: z.coerce.number()
}),
response: {
200: z.object({
secretVersions: secretRawSchema
.omit({ secretValue: true })
.extend({
secretValueHidden: z.boolean()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretVersions = await server.services.secret.getSecretVersions({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
limit: req.query.limit,
offset: req.query.offset,
secretId: req.params.secretId
});
return { secretVersions };
}
});
server.route({
method: "GET",
url: "/secret-versions/:secretId/value/:version",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
secretId: z.string(),
version: z.string()
}),
response: {
200: z.object({
value: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { version, secretId } = req.params;
const [secretVersion] = await server.services.secret.getSecretVersions({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretId,
secretVersions: [version]
});
if (!secretVersion)
throw new NotFoundError({
message: `Could not find secret version "${version}" for secret with ID "${secretId}`
});
await server.services.auditLog.createAuditLog({
projectId: secretVersion.workspace,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE,
metadata: {
secretId,
version
}
}
});
return { value: secretVersion.secretValue };
}
});
}; };
@@ -0,0 +1,298 @@
import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, ENVIRONMENTS } 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 { AuthMode } from "@app/services/auth/auth-type";
export const registerDeprecatedProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/environments/:envId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Environments],
description: "Get Environment",
security: [
{
bearerAuth: []
}
],
params: z.object({
// NOTE(daniel): workspaceId isn't used, but we need to keep it for backwards compatibility. The endpoint defined below, uses no project ID, and is takes a pure environment ID.
workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.projectId),
envId: z.string().trim().describe(ENVIRONMENTS.GET.id)
}),
response: {
200: z.object({
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const environment = await server.services.projectEnv.getEnvironmentById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.envId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.GET_ENVIRONMENT,
metadata: {
id: environment.id
}
}
});
return { environment };
}
});
server.route({
method: "GET",
url: "/environments/:envId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Environments],
description: "Get Environment by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
envId: z.string().trim().describe(ENVIRONMENTS.GET.id)
}),
response: {
200: z.object({
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const environment = await server.services.projectEnv.getEnvironmentById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.envId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.GET_ENVIRONMENT,
metadata: {
id: environment.id
}
}
});
return { environment };
}
});
server.route({
method: "POST",
url: "/:workspaceId/environments",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Environments],
description: "Create environment",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(ENVIRONMENTS.CREATE.projectId)
}),
body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position),
slug: slugSchema({ max: 64 }).describe(ENVIRONMENTS.CREATE.slug)
}),
response: {
200: z.object({
message: z.string(),
workspace: z.string(),
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const environment = await server.services.projectEnv.createEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.CREATE_ENVIRONMENT,
metadata: {
name: environment.name,
slug: environment.slug
}
}
});
return {
message: "Successfully created new environment",
workspace: req.params.workspaceId,
environment
};
}
});
server.route({
method: "PATCH",
url: "/:workspaceId/environments/:id",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Environments],
description: "Update environment",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(ENVIRONMENTS.UPDATE.projectId),
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
}),
body: z.object({
slug: slugSchema({ max: 64 }).optional().describe(ENVIRONMENTS.UPDATE.slug),
name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
}),
response: {
200: z.object({
message: z.string(),
workspace: z.string(),
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { environment, old } = await server.services.projectEnv.updateEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
id: req.params.id,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.UPDATE_ENVIRONMENT,
metadata: {
oldName: old.name,
oldSlug: old.slug,
oldPos: old.position,
newName: environment.name,
newSlug: environment.slug,
newPos: environment.position
}
}
});
return {
message: "Successfully updated environment",
workspace: req.params.workspaceId,
environment
};
}
});
server.route({
method: "DELETE",
url: "/:workspaceId/environments/:id",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Environments],
description: "Delete environment",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(ENVIRONMENTS.DELETE.projectId),
id: z.string().trim().describe(ENVIRONMENTS.DELETE.id)
}),
response: {
200: z.object({
message: z.string(),
workspace: z.string(),
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const environment = await server.services.projectEnv.deleteEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
id: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.DELETE_ENVIRONMENT,
metadata: {
slug: environment.slug,
name: environment.name
}
}
});
return {
message: "Successfully deleted environment",
workspace: req.params.workspaceId,
environment
};
}
});
};
@@ -0,0 +1,378 @@
import { z } from "zod";
import {
OrgMembershipsSchema,
ProjectMembershipsSchema,
ProjectUserMembershipRolesSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
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";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/memberships",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectUsers],
description: "Return project user memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIPS.projectId)
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.extend({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true,
username: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
)
})
.omit({ updatedAt: true })
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const memberships = await server.services.projectMembership.getProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return { memberships };
}
});
server.route({
method: "GET",
url: "/:workspaceId/memberships/:membershipId",
config: {
rateLimit: readLimit
},
schema: {
description: "Return project user membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId),
membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId)
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema.extend({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true,
username: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
)
}).omit({ updatedAt: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const membership = await server.services.projectMembership.getProjectMembershipById({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
id: req.params.membershipId
});
return { membership };
}
});
server.route({
method: "POST",
url: "/:workspaceId/memberships/details",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectUsers],
description: "Return project user memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId)
}),
body: z.object({
username: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.username)
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema.extend({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
)
}).omit({ createdAt: true, updatedAt: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const membership = await server.services.projectMembership.getProjectMembershipByUsername({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
username: req.body.username
});
return { membership };
}
});
server.route({
method: "POST",
url: "/:workspaceId/memberships",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
members: z
.object({
orgMembershipId: z.string().trim(),
workspaceEncryptedKey: z.string().trim(),
workspaceEncryptedNonce: z.string().trim()
})
.array()
.min(1)
}),
response: {
200: z.object({
success: z.boolean(),
data: OrgMembershipsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.projectMembership.addUsersToProject({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
members: req.body.members
});
await server.services.auditLog.createAuditLog({
projectId: req.params.workspaceId,
...req.auditLogInfo,
event: {
type: EventType.ADD_BATCH_PROJECT_MEMBER,
metadata: data.map(({ userId }) => ({
userId: userId || "",
email: ""
}))
}
});
return { data, success: true };
}
});
server.route({
method: "PATCH",
url: "/:workspaceId/memberships/:membershipId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectUsers],
description: "Update project user membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.projectId),
membershipId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.membershipId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string(),
isTemporary: z.literal(false).default(false)
}),
z.object({
role: z.string(),
isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime()
})
])
)
.min(1)
.refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least one long lived role is required")
.describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.roles)
}),
response: {
200: z.object({
roles: ProjectUserMembershipRolesSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const roles = await server.services.projectMembership.updateProjectMembership({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
membershipId: req.params.membershipId,
roles: req.body.roles
});
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: req.params.workspaceId,
// event: {
// type: EventType.UPDATE_USER_WORKSPACE_ROLE,
// metadata: {
// userId: membership.userId,
// newRole: req.body.role,
// oldRole: membership.role,
// email: ""
// }
// }
// });
return { roles };
}
});
server.route({
method: "DELETE",
url: "/:workspaceId/memberships/:membershipId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete project user membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim(),
membershipId: z.string().trim()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const membership = await server.services.projectMembership.deleteProjectMembership({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
membershipId: req.params.membershipId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.REMOVE_PROJECT_MEMBER,
metadata: {
userId: membership.userId,
email: ""
}
}
});
return { membership };
}
});
};
@@ -0,0 +1,728 @@
import { z } from "zod";
import {
IntegrationsSchema,
ProjectRolesSchema,
ProjectSlackConfigsSchema,
ProjectSshConfigsSchema,
ProjectType,
SortDirection
} from "@app/db/schemas";
import { ProjectMicrosoftTeamsConfigsSchema } from "@app/db/schemas/project-microsoft-teams-configs";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { re2Validator } from "@app/lib/zod";
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";
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
const projectWithEnv = SanitizedProjectSchema.merge(
z.object({
_id: z.string(),
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
})
);
export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
includeRoles: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true"),
type: z.nativeEnum(ProjectType).optional()
}),
response: {
200: z.object({
workspaces: projectWithEnv
.extend({
roles: ProjectRolesSchema.array().optional()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaces = await server.services.project.getProjects({
includeRoles: req.query.includeRoles,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
type: req.query.type
});
return { workspaces };
}
});
server.route({
method: "GET",
url: "/:workspaceId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Projects],
description: "Get project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.GET.projectId)
}),
response: {
200: z.object({
workspace: projectWithEnv.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.getAProject({
filter: {
type: ProjectFilterType.ID,
projectId: req.params.workspaceId
},
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
return { workspace };
}
});
server.route({
method: "DELETE",
url: "/:workspaceId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Projects],
description: "Delete project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.DELETE.projectId)
}),
response: {
200: z.object({
workspace: SanitizedProjectSchema.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.deleteProject({
filter: {
type: ProjectFilterType.ID,
projectId: req.params.workspaceId
},
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.workspaceId,
event: {
type: EventType.DELETE_PROJECT,
metadata: workspace
}
});
return { workspace };
}
});
server.route({
method: "PATCH",
url: "/:workspaceId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Projects],
description: "Update project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.UPDATE.projectId)
}),
body: z.object({
name: z
.string()
.trim()
.max(64, { message: "Name must be 64 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.name),
description: z
.string()
.trim()
.max(256, { message: "Description must be 256 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection),
slug: z
.string()
.trim()
.max(64, { message: "Slug must be 64 characters or fewer" })
.refine(re2Validator(/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/), {
message:
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
})
.optional()
.describe(PROJECTS.UPDATE.slug),
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing),
showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy),
defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct),
secretDetectionIgnoreValues: z
.array(z.string())
.optional()
.describe(PROJECTS.UPDATE.secretDetectionIgnoreValues)
}),
response: {
200: z.object({
workspace: SanitizedProjectSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.updateProject({
filter: {
type: ProjectFilterType.ID,
projectId: req.params.workspaceId
},
update: {
name: req.body.name,
description: req.body.description,
autoCapitalization: req.body.autoCapitalization,
defaultProduct: req.body.defaultProduct,
hasDeleteProtection: req.body.hasDeleteProtection,
slug: req.body.slug,
secretSharing: req.body.secretSharing,
showSnapshotsLegacy: req.body.showSnapshotsLegacy,
secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues
},
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return {
workspace
};
}
});
server.route({
method: "PUT",
url: "/:workspaceSlug/audit-logs-retention",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceSlug: z.string().trim()
}),
body: z.object({
auditLogsRetentionDays: z.number().min(0)
}),
response: {
200: z.object({
message: z.string(),
workspace: SanitizedProjectSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.updateAuditLogsRetention({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
filter: {
type: ProjectFilterType.SLUG,
slug: req.params.workspaceSlug,
orgId: req.permission.orgId
},
auditLogsRetentionDays: req.body.auditLogsRetentionDays
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: workspace.id,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return {
message: "Successfully updated project's audit logs retention period",
workspace
};
}
});
server.route({
method: "GET",
url: "/:workspaceId/integrations",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Integrations],
description: "List integrations for a project.",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.projectId)
}),
response: {
200: z.object({
integrations: IntegrationsSchema.merge(
z.object({
environment: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
})
})
).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integrations = await server.services.integration.listIntegrationByProject({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return { integrations };
}
});
server.route({
method: "GET",
url: "/:workspaceId/authorizations",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Integrations],
description: "List integration auth objects for a workspace.",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.projectId)
}),
response: {
200: z.object({
authorizations: integrationAuthPubSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return { authorizations };
}
});
server.route({
method: "GET",
url: "/:workspaceId/ssh-config",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: ProjectSshConfigsSchema.pick({
id: true,
createdAt: true,
updatedAt: true,
projectId: true,
defaultUserSshCaId: true,
defaultHostSshCaId: true
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const sshConfig = await server.services.project.getProjectSshConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: sshConfig.projectId,
event: {
type: EventType.GET_PROJECT_SSH_CONFIG,
metadata: {
id: sshConfig.id,
projectId: sshConfig.projectId
}
}
});
return sshConfig;
}
});
server.route({
method: "PATCH",
url: "/:workspaceId/ssh-config",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
defaultUserSshCaId: z.string().optional(),
defaultHostSshCaId: z.string().optional()
}),
response: {
200: ProjectSshConfigsSchema.pick({
id: true,
createdAt: true,
updatedAt: true,
projectId: true,
defaultUserSshCaId: true,
defaultHostSshCaId: true
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const sshConfig = await server.services.project.updateProjectSshConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: sshConfig.projectId,
event: {
type: EventType.UPDATE_PROJECT_SSH_CONFIG,
metadata: {
id: sshConfig.id,
projectId: sshConfig.projectId,
defaultUserSshCaId: sshConfig.defaultUserSshCaId,
defaultHostSshCaId: sshConfig.defaultHostSshCaId
}
}
});
return sshConfig;
}
});
server.route({
method: "GET",
url: "/:workspaceId/workflow-integration-config/:integration",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim(),
integration: z.nativeEnum(WorkflowIntegration)
}),
response: {
200: z.discriminatedUnion("integration", [
ProjectSlackConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
integrationId: z.string()
})
),
ProjectMicrosoftTeamsConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string()
})
)
])
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const config = await server.services.project.getProjectWorkflowIntegrationConfig({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
integration: req.params.integration
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
metadata: {
id: config.id,
integration: config.integration
}
}
});
return config;
}
});
server.route({
method: "DELETE",
url: "/:projectId/workflow-integration/:integration/:integrationId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
projectId: z.string().trim(),
integration: z.nativeEnum(WorkflowIntegration),
integrationId: z.string()
}),
response: {
200: z.object({
integrationConfig: z.object({
id: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const deletedIntegration = await server.services.project.deleteProjectWorkflowIntegration({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
integration: req.params.integration,
integrationId: req.params.integrationId
});
return {
integrationConfig: deletedIntegration
};
}
});
server.route({
method: "PUT",
url: "/:workspaceId/workflow-integration",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.discriminatedUnion("integration", [
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
integrationId: z.string(),
accessRequestChannels: validateSlackChannelsField,
secretRequestChannels: validateSlackChannelsField,
isAccessRequestNotificationEnabled: z.boolean(),
isSecretRequestNotificationEnabled: z.boolean()
}),
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string(),
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
secretRequestChannels: validateMicrosoftTeamsChannelsSchema,
isAccessRequestNotificationEnabled: z.boolean(),
isSecretRequestNotificationEnabled: z.boolean()
})
]),
response: {
200: z.discriminatedUnion("integration", [
ProjectSlackConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
accessRequestChannels: true,
isSecretRequestNotificationEnabled: true,
secretRequestChannels: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.SLACK),
integrationId: z.string()
})
),
ProjectMicrosoftTeamsConfigsSchema.pick({
id: true,
isAccessRequestNotificationEnabled: true,
isSecretRequestNotificationEnabled: true
}).merge(
z.object({
integration: z.literal(WorkflowIntegration.MICROSOFT_TEAMS),
integrationId: z.string(),
accessRequestChannels: validateMicrosoftTeamsChannelsSchema,
secretRequestChannels: validateMicrosoftTeamsChannelsSchema
})
)
])
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workflowIntegrationConfig = await server.services.project.updateProjectWorkflowIntegration({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT_WORKFLOW_INTEGRATION_CONFIG,
metadata: {
id: workflowIntegrationConfig.id,
integrationId: workflowIntegrationConfig.integrationId,
integration: workflowIntegrationConfig.integration,
isAccessRequestNotificationEnabled: workflowIntegrationConfig.isAccessRequestNotificationEnabled,
accessRequestChannels: workflowIntegrationConfig.accessRequestChannels,
isSecretRequestNotificationEnabled: workflowIntegrationConfig.isSecretRequestNotificationEnabled,
secretRequestChannels: workflowIntegrationConfig.secretRequestChannels
}
}
});
return workflowIntegrationConfig;
}
});
server.route({
method: "POST",
url: "/search",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
limit: z.number().default(100),
offset: z.number().default(0),
type: z.nativeEnum(ProjectType).optional(),
orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME),
orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC),
name: z
.string()
.trim()
.refine((val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val), {
message: "Invalid pattern: only alphanumeric characters, - are allowed."
})
.optional()
}),
response: {
200: z.object({
projects: SanitizedProjectSchema.extend({ isMember: z.boolean() }).array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { docs: projects, totalCount } = await server.services.project.searchProjects({
permission: req.permission,
...req.body
});
return { projects, totalCount };
}
});
};
@@ -0,0 +1,444 @@
import { z } from "zod";
import { SecretFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, FOLDERS } from "@app/lib/api-docs";
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
import { isValidFolderName } from "@app/lib/validator";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { booleanSchema } from "../sanitizedSchemas";
export const registerDeprecatedSecretFolderRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
description: "Create folders",
security: [
{
bearerAuth: []
}
],
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.CREATE.projectId),
environment: z.string().trim().describe(FOLDERS.CREATE.environment),
name: z
.string()
.trim()
.describe(FOLDERS.CREATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.path)
.optional(),
// backward compatibility with cli
directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
}),
response: {
200: z.object({
folder: SecretFoldersSchema.extend({
path: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.createFolder({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
path,
description: req.body.description
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.CREATE_FOLDER,
metadata: {
environment: req.body.environment,
folderId: folder.id,
folderName: folder.name,
folderPath: path,
...(req.body.description ? { description: req.body.description } : {})
}
}
});
return { folder };
}
});
server.route({
url: "/:folderId",
method: "PATCH",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
description: "Update folder",
security: [
{
bearerAuth: []
}
],
params: z.object({
// old way this was name
folderId: z.string().describe(FOLDERS.UPDATE.folderId)
}),
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.projectId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z
.string()
.trim()
.describe(FOLDERS.UPDATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path)
.optional(),
// backward compatibility with cli
directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
}),
response: {
200: z.object({
folder: SecretFoldersSchema.extend({
path: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const path = req.body.path || req.body.directory || "/";
const { folder, old } = await server.services.folder.updateFolder({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
id: req.params.folderId,
path
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.UPDATE_FOLDER,
metadata: {
environment: req.body.environment,
folderId: folder.id,
folderPath: path,
newFolderName: folder.name,
oldFolderName: old.name
}
}
});
return { folder };
}
});
server.route({
url: "/batch",
method: "PATCH",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
description: "Update folders by batch",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectSlug: z.string().trim().describe(FOLDERS.UPDATE.projectSlug),
folders: z
.object({
id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z
.string()
.trim()
.describe(FOLDERS.UPDATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash)
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path),
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
})
.array()
.min(1)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { newFolders, oldFolders, projectId } = await server.services.folder.updateManyFolders({
...req.body,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await Promise.all(
req.body.folders.map(async (folder, index) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.UPDATE_FOLDER,
metadata: {
environment: oldFolders[index].envId,
folderId: oldFolders[index].id,
folderPath: folder.path,
newFolderName: newFolders[index].name,
oldFolderName: oldFolders[index].name
}
}
});
})
);
return { folders: newFolders };
}
});
// TODO(daniel): Expose this route in api reference and write docs for it.
server.route({
method: "DELETE",
url: "/:folderIdOrName",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
description: "Delete a folder",
security: [
{
bearerAuth: []
}
],
params: z.object({
folderIdOrName: z.string().describe(FOLDERS.DELETE.folderIdOrName)
}),
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.DELETE.projectId),
environment: z.string().trim().describe(FOLDERS.DELETE.environment),
path: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.path)
.optional(),
// keep this here as cli need directory
directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.directory)
.optional()
}),
response: {
200: z.object({
folder: SecretFoldersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.deleteFolder({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
idOrName: req.params.folderIdOrName,
path
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.DELETE_FOLDER,
metadata: {
environment: req.body.environment,
folderId: folder.id,
folderPath: path,
folderName: folder.name
}
}
});
return { folder };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
description: "Get folders",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
workspaceId: z.string().trim().describe(FOLDERS.LIST.projectId),
environment: z.string().trim().describe(FOLDERS.LIST.environment),
lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified),
path: z
.string()
.trim()
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.LIST.path)
.optional(),
// backward compatibility with cli
directory: z
.string()
.trim()
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.LIST.directory)
.optional(),
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.extend({
relativePath: z.string().optional()
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const path = req.query.path || req.query.directory || "/";
const folders = await server.services.folder.getFolders({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId,
path
});
return { folders };
}
});
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
description: "Get folder by id",
security: [
{
bearerAuth: []
}
],
params: z.object({
id: z.string().trim().describe(FOLDERS.GET_BY_ID.folderId)
}),
response: {
200: z.object({
folder: SecretFoldersSchema.extend({
environment: z.object({
envId: z.string(),
envName: z.string(),
envSlug: z.string()
}),
path: z.string(),
projectId: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const folder = await server.services.folder.getFolderById({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
return { folder };
}
});
};
@@ -0,0 +1,472 @@
import { z } from "zod";
import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, SECRET_IMPORTS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { secretRawSchema } from "../sanitizedSchemas";
export const registerDeprecatedSecretImportRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretImports],
description: "Create secret imports",
security: [
{
bearerAuth: []
}
],
body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.CREATE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.path),
import: z.object({
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
}),
isReplication: z.boolean().default(false).describe(SECRET_IMPORTS.CREATE.isReplication)
}),
response: {
200: z.object({
message: z.string(),
secretImport: SecretImportsSchema.omit({ importEnv: true }).merge(
z.object({
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretImport = await server.services.secretImport.createImport({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
data: req.body.import
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: secretImport.id,
folderId: secretImport.folderId,
importFromSecretPath: secretImport.importPath,
importFromEnvironment: secretImport.importEnv.slug,
importToEnvironment: req.body.environment,
importToSecretPath: req.body.path
}
}
});
return { message: "Successfully created secret import", secretImport };
}
});
server.route({
method: "PATCH",
url: "/:secretImportId",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretImports],
description: "Update secret imports",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId)
}),
body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path),
import: z.object({
environment: z.string().trim().optional().describe(SECRET_IMPORTS.UPDATE.import.environment),
path: z
.string()
.trim()
.optional()
.transform((val) => (val ? removeTrailingSlash(val) : val))
.describe(SECRET_IMPORTS.UPDATE.import.path),
position: z.number().optional().describe(SECRET_IMPORTS.UPDATE.import.position)
})
}),
response: {
200: z.object({
message: z.string(),
secretImport: SecretImportsSchema.omit({ importEnv: true }).merge(
z.object({
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretImport = await server.services.secretImport.updateImport({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretImportId,
...req.body,
projectId: req.body.workspaceId,
data: req.body.import
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
secretImportId: secretImport.id,
folderId: secretImport.folderId,
position: secretImport.position,
importToEnvironment: req.body.environment,
importToSecretPath: req.body.path
}
}
});
return { message: "Successfully updated secret import", secretImport };
}
});
server.route({
method: "DELETE",
url: "/:secretImportId",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretImports],
description: "Delete secret imports",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretImportId: z.string().trim().describe(SECRET_IMPORTS.DELETE.secretImportId)
}),
body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.DELETE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.DELETE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.DELETE.path)
}),
response: {
200: z.object({
message: z.string(),
secretImport: SecretImportsSchema.omit({ importEnv: true }).merge(
z.object({
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretImport = await server.services.secretImport.deleteImport({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretImportId,
...req.body,
projectId: req.body.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
event: {
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
secretImportId: secretImport.id,
folderId: secretImport.folderId,
importFromEnvironment: secretImport.importEnv.slug,
importFromSecretPath: secretImport.importPath,
importToEnvironment: req.body.environment,
importToSecretPath: req.body.path
}
}
});
return { message: "Successfully deleted secret import", secretImport };
}
});
server.route({
method: "POST",
url: "/:secretImportId/replication-resync",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Resync secret replication of secret imports",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId)
}),
body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path)
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { message } = await server.services.secretImport.resyncSecretImportReplication({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretImportId,
...req.body,
projectId: req.body.workspaceId
});
return { message };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretImports],
description: "Get secret imports",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.LIST.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.LIST.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.LIST.path)
}),
response: {
200: z.object({
message: z.string(),
secretImports: SecretImportsSchema.omit({ importEnv: true })
.extend({
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretImports = await server.services.secretImport.getImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.workspaceId,
event: {
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment: req.query.environment,
folderId: secretImports?.[0]?.folderId,
numberOfImports: secretImports.length
}
}
});
return { message: "Successfully fetched secret imports", secretImports };
}
});
server.route({
url: "/:secretImportId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretImports],
description: "Get single secret import",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretImportId: z.string().trim().describe(SECRET_IMPORTS.GET.secretImportId)
}),
response: {
200: z.object({
secretImport: SecretImportsSchema.omit({ importEnv: true }).extend({
environment: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
projectId: z.string(),
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }),
secretPath: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretImport = await server.services.secretImport.getImportById({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretImportId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretImport.projectId,
event: {
type: EventType.GET_SECRET_IMPORT,
metadata: {
secretImportId: secretImport.id,
folderId: secretImport.folderId
}
}
});
return { secretImport };
}
});
server.route({
url: "/secrets",
method: "GET",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
secrets: z
.object({
secretPath: z.string(),
environment: z.string(),
environmentInfo: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
folderId: z.string().optional(),
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const importedSecrets = await server.services.secretImport.getSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
});
return { secrets: importedSecrets };
}
});
server.route({
url: "/secrets/raw",
method: "GET",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretImports],
querystring: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
secrets: z
.object({
secretPath: z.string(),
environment: z.string(),
environmentInfo: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
folderId: z.string().optional(),
secrets: secretRawSchema.array()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
});
return { secrets: importedSecrets };
}
});
};
@@ -0,0 +1,213 @@
import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas";
import { ApiDocsTags, SECRET_TAGS } 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 { AuthMode } from "@app/services/auth/auth-type";
export const registerDeprecatedSecretTagRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:projectId/tags",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
}),
response: {
200: z.object({
workspaceTags: SecretTagsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTags = await server.services.secretTag.getProjectTags({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId
});
return { workspaceTags };
}
});
server.route({
method: "GET",
url: "/:projectId/tags/:tagId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId),
tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId)
}),
response: {
200: z.object({
// akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.getTagById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.tagId
});
return { workspaceTag };
}
});
server.route({
method: "GET",
url: "/:projectId/tags/slug/:tagSlug",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId),
tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug)
}),
response: {
200: z.object({
// akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.getTagBySlug({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
slug: req.params.tagSlug,
projectId: req.params.projectId
});
return { workspaceTag };
}
});
server.route({
method: "POST",
url: "/:projectId/tags",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}),
body: z.object({
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.CREATE.slug),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.createTag({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
...req.body
});
return { workspaceTag };
}
});
server.route({
method: "PATCH",
url: "/:projectId/tags/:tagId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId),
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}),
body: z.object({
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.UPDATE.slug),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.updateTag({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
id: req.params.tagId
});
return { workspaceTag };
}
});
server.route({
method: "DELETE",
url: "/:projectId/tags/:tagId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Folders],
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.deleteTag({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.tagId
});
return { workspaceTag };
}
});
};
@@ -8,7 +8,7 @@
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import { FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { IncomingMessage } from "http"; import { IncomingMessage } from "http";
import LdapStrategy from "passport-ldapauth"; import LdapStrategy from "passport-ldapauth";
import { z } from "zod"; import { z } from "zod";
@@ -135,19 +135,26 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
}) })
} }
}, },
preValidation: passport.authenticate("ldapauth", { preValidation: [
failWithError: true, (req, res) => {
session: false const passportAuth = (request: FastifyRequest, reply: FastifyReply) =>
}) as any, (
passport.authenticate("ldapauth", {
failWithError: true,
session: false
}) as any
)(request, reply);
errorHandler: (error) => { const { identityId, username } = req.body;
if (error.name === "AuthenticationError") { return server.services.identityLdapAuth.withLdapLockout(
throw new UnauthorizedError({ message: "Invalid credentials" }); {
identityId,
username
},
() => passportAuth(req, res)
);
} }
],
throw error;
},
handler: async (req) => { handler: async (req) => {
if (!req.passportMachineIdentity?.identityId) { if (!req.passportMachineIdentity?.identityId) {
throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." }); throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." });
@@ -241,7 +248,21 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
.int() .int()
.min(0) .min(0)
.default(0) .default(0)
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit),
lockoutEnabled: z.boolean().default(true).describe(LDAP_AUTH.ATTACH.lockoutEnabled),
lockoutThreshold: z.number().min(1).max(30).default(3).describe(LDAP_AUTH.ATTACH.lockoutThreshold),
lockoutDurationSeconds: z
.number()
.min(30)
.max(86400)
.default(300)
.describe(LDAP_AUTH.ATTACH.lockoutDurationSeconds),
lockoutCounterResetSeconds: z
.number()
.min(5)
.max(3600)
.default(30)
.describe(LDAP_AUTH.ATTACH.lockoutCounterResetSeconds)
}) })
.refine( .refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL, (val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
@@ -291,7 +312,21 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
.int() .int()
.min(0) .min(0)
.default(0) .default(0)
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit),
lockoutEnabled: z.boolean().default(true).describe(LDAP_AUTH.ATTACH.lockoutEnabled),
lockoutThreshold: z.number().min(1).max(30).default(3).describe(LDAP_AUTH.ATTACH.lockoutThreshold),
lockoutDurationSeconds: z
.number()
.min(30)
.max(86400)
.default(300)
.describe(LDAP_AUTH.ATTACH.lockoutDurationSeconds),
lockoutCounterResetSeconds: z
.number()
.min(5)
.max(3600)
.default(30)
.describe(LDAP_AUTH.ATTACH.lockoutCounterResetSeconds)
}) })
.refine( .refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL, (val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
@@ -331,7 +366,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
allowedFields: req.body.allowedFields, allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId templateId: identityLdapAuth.templateId,
lockoutEnabled: identityLdapAuth.lockoutEnabled,
lockoutThreshold: identityLdapAuth.lockoutThreshold,
lockoutDurationSeconds: identityLdapAuth.lockoutDurationSeconds,
lockoutCounterResetSeconds: identityLdapAuth.lockoutCounterResetSeconds
} }
} }
}); });
@@ -395,7 +434,21 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
.max(315360000) .max(315360000)
.min(0) .min(0)
.optional() .optional()
.describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL) .describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL),
lockoutEnabled: z.boolean().optional().describe(LDAP_AUTH.UPDATE.lockoutEnabled),
lockoutThreshold: z.number().min(1).max(30).optional().describe(LDAP_AUTH.UPDATE.lockoutThreshold),
lockoutDurationSeconds: z
.number()
.min(30)
.max(86400)
.optional()
.describe(LDAP_AUTH.UPDATE.lockoutDurationSeconds),
lockoutCounterResetSeconds: z
.number()
.min(5)
.max(3600)
.optional()
.describe(LDAP_AUTH.UPDATE.lockoutCounterResetSeconds)
}) })
.refine( .refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
@@ -434,7 +487,11 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[], accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
allowedFields: req.body.allowedFields, allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId templateId: identityLdapAuth.templateId,
lockoutEnabled: identityLdapAuth.lockoutEnabled,
lockoutThreshold: identityLdapAuth.lockoutThreshold,
lockoutDurationSeconds: identityLdapAuth.lockoutDurationSeconds,
lockoutCounterResetSeconds: identityLdapAuth.lockoutCounterResetSeconds
} }
} }
}); });
@@ -553,4 +610,53 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
return { identityLdapAuth }; return { identityLdapAuth };
} }
}); });
server.route({
method: "POST",
url: "/ldap-auth/identities/:identityId/clear-lockouts",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.LdapAuth],
description: "Clear LDAP Auth Lockouts for identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(LDAP_AUTH.CLEAR_CLIENT_LOCKOUTS.identityId)
}),
response: {
200: z.object({
deleted: z.number()
})
}
},
handler: async (req) => {
const clearLockoutsData = await server.services.identityLdapAuth.clearLdapAuthLockouts({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: clearLockoutsData.orgId,
event: {
type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS,
metadata: {
identityId: clearLockoutsData.identityId
}
}
});
return clearLockoutsData;
}
});
}; };
+27 -8
View File
@@ -13,8 +13,15 @@ import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertRouter } from "./certificate-router"; import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
import { registerDeprecatedSecretFolderRouter } from "./deprecated-secret-folder-router";
import { registerDeprecatedSecretImportRouter } from "./deprecated-secret-import-router";
import { registerDeprecatedSecretTagRouter } from "./deprecated-secret-tag-router";
import { registerEventRouter } from "./event-router"; import { registerEventRouter } from "./event-router";
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router"; import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
import { registerGroupProjectRouter } from "./group-project-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router"; import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
@@ -25,6 +32,7 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -45,8 +53,6 @@ import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router"; import { registerProjectRouter } from "./project-router";
import { SECRET_REMINDER_REGISTER_ROUTER_MAP } from "./reminder-routers"; import { SECRET_REMINDER_REGISTER_ROUTER_MAP } from "./reminder-routers";
import { registerSecretFolderRouter } from "./secret-folder-router";
import { registerSecretImportRouter } from "./secret-import-router";
import { registerSecretRequestsRouter } from "./secret-requests-router"; import { registerSecretRequestsRouter } from "./secret-requests-router";
import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router"; import { registerSecretTagRouter } from "./secret-tag-router";
@@ -87,8 +93,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerNotificationRouter, { prefix: "/notifications" }); await server.register(registerNotificationRouter, { prefix: "/notifications" });
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" }); await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
await server.register(registerUserActionRouter, { prefix: "/user-action" }); await server.register(registerUserActionRouter, { prefix: "/user-action" });
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" }); await server.register(registerDeprecatedSecretImportRouter, { prefix: "/secret-imports" });
await server.register(registerSecretFolderRouter, { prefix: "/folders" }); await server.register(registerDeprecatedSecretFolderRouter, { prefix: "/folders" });
await server.register( await server.register(
async (workflowIntegrationRouter) => { async (workflowIntegrationRouter) => {
@@ -101,15 +107,28 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register( await server.register(
async (projectRouter) => { async (projectRouter) => {
await projectRouter.register(registerProjectRouter); await projectRouter.register(registerDeprecatedProjectRouter);
await projectRouter.register(registerProjectEnvRouter); await projectRouter.register(registerDeprecatedProjectEnvRouter);
// depreciated completed in use
await projectRouter.register(registerProjectKeyRouter); await projectRouter.register(registerProjectKeyRouter);
await projectRouter.register(registerProjectMembershipRouter); await projectRouter.register(registerDeprecatedProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter); await projectRouter.register(registerDeprecatedSecretTagRouter);
}, },
{ prefix: "/workspace" } { prefix: "/workspace" }
); );
await server.register(
async (projectRouter) => {
await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerProjectEnvRouter);
await projectRouter.register(registerSecretTagRouter);
await projectRouter.register(registerGroupProjectRouter);
await projectRouter.register(registerIdentityProjectRouter);
},
{ prefix: "/projects" }
);
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
@@ -3,8 +3,10 @@ import { z } from "zod";
import { UserNotificationsSchema } from "@app/db/schemas/user-notifications"; import { UserNotificationsSchema } from "@app/db/schemas/user-notifications";
import { UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
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";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerNotificationRouter = async (server: FastifyZodProvider) => { export const registerNotificationRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -97,6 +99,16 @@ export const registerNotificationRouter = async (server: FastifyZodProvider) =>
...req.body ...req.body
}); });
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.NotificationUpdated,
distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: {
notificationId: req.params.notificationId,
...req.body
}
});
return { notification }; return { notification };
} }
}); });
@@ -11,58 +11,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:workspaceId/environments/:envId", url: "/:projectId/environments/:envId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Environments],
description: "Get Environment",
security: [
{
bearerAuth: []
}
],
params: z.object({
// NOTE(daniel): workspaceId isn't used, but we need to keep it for backwards compatibility. The endpoint defined below, uses no project ID, and is takes a pure environment ID.
workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.workspaceId),
envId: z.string().trim().describe(ENVIRONMENTS.GET.id)
}),
response: {
200: z.object({
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const environment = await server.services.projectEnv.getEnvironmentById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.envId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.GET_ENVIRONMENT,
metadata: {
id: environment.id
}
}
});
return { environment };
}
});
server.route({
method: "GET",
url: "/environments/:envId",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -76,7 +25,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
envId: z.string().trim().describe(ENVIRONMENTS.GET.id) envId: z.string().trim().describe(ENVIRONMENTS.GET.id),
projectId: z.string().trim().describe(ENVIRONMENTS.GET.projectId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -111,7 +61,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/:workspaceId/environments", url: "/:projectId/environments",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -125,7 +75,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(ENVIRONMENTS.CREATE.workspaceId) projectId: z.string().trim().describe(ENVIRONMENTS.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
@@ -135,7 +85,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
workspace: z.string(), projectId: z.string(),
environment: ProjectEnvironmentsSchema environment: ProjectEnvironmentsSchema
}) })
} }
@@ -147,7 +97,7 @@ export const registerProjectEnvRouter = 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,
projectId: req.params.workspaceId, projectId: req.params.projectId,
...req.body ...req.body
}); });
@@ -164,7 +114,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
}); });
return { return {
message: "Successfully created new environment", message: "Successfully created new environment",
workspace: req.params.workspaceId, projectId: req.params.projectId,
environment environment
}; };
} }
@@ -172,7 +122,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:workspaceId/environments/:id", url: "/:projectId/environments/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -186,7 +136,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(ENVIRONMENTS.UPDATE.workspaceId), projectId: z.string().trim().describe(ENVIRONMENTS.UPDATE.projectId),
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id) id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
}), }),
body: z.object({ body: z.object({
@@ -197,7 +147,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
workspace: z.string(), projectId: z.string(),
environment: ProjectEnvironmentsSchema environment: ProjectEnvironmentsSchema
}) })
} }
@@ -209,7 +159,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.projectId,
id: req.params.id, id: req.params.id,
...req.body ...req.body
}); });
@@ -232,7 +182,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
return { return {
message: "Successfully updated environment", message: "Successfully updated environment",
workspace: req.params.workspaceId, projectId: req.params.projectId,
environment environment
}; };
} }
@@ -240,7 +190,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:workspaceId/environments/:id", url: "/:projectId/environments/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -254,13 +204,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(ENVIRONMENTS.DELETE.workspaceId), projectId: z.string().trim().describe(ENVIRONMENTS.DELETE.projectId),
id: z.string().trim().describe(ENVIRONMENTS.DELETE.id) id: z.string().trim().describe(ENVIRONMENTS.DELETE.id)
}), }),
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
workspace: z.string(), projectId: z.string(),
environment: ProjectEnvironmentsSchema environment: ProjectEnvironmentsSchema
}) })
} }
@@ -272,7 +222,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.projectId,
id: req.params.id id: req.params.id
}); });
@@ -290,7 +240,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
return { return {
message: "Successfully deleted environment", message: "Successfully deleted environment",
workspace: req.params.workspaceId, projectId: req.params.projectId,
environment environment
}; };
} }
@@ -1,7 +1,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
OrgMembershipsSchema, OrgMembershipRole,
ProjectMembershipRole,
ProjectMembershipsSchema, ProjectMembershipsSchema,
ProjectUserMembershipRolesSchema, ProjectUserMembershipRolesSchema,
UserEncryptionKeysSchema, UserEncryptionKeysSchema,
@@ -18,7 +19,7 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:workspaceId/memberships", url: "/:projectId/memberships",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -32,7 +33,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIPS.workspaceId) projectId: z.string().trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIPS.projectId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -71,7 +72,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type, actor: req.permission.type,
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 { memberships }; return { memberships };
} }
@@ -79,7 +80,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
server.route({ server.route({
method: "GET", method: "GET",
url: "/:workspaceId/memberships/:membershipId", url: "/:projectId/memberships/:membershipId",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -91,7 +92,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId), projectId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId),
membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId) membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId)
}), }),
response: { response: {
@@ -129,7 +130,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.projectId,
id: req.params.membershipId id: req.params.membershipId
}); });
return { membership }; return { membership };
@@ -138,7 +139,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
server.route({ server.route({
method: "POST", method: "POST",
url: "/:workspaceId/memberships/details", url: "/:projectId/memberships/details",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -152,7 +153,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId) projectId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.projectId)
}), }),
body: z.object({ body: z.object({
username: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.username) username: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.username)
@@ -191,7 +192,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.projectId,
username: req.body.username username: req.body.username
}); });
return { membership }; return { membership };
@@ -200,61 +201,83 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
server.route({ server.route({
method: "POST", method: "POST",
url: "/:workspaceId/memberships", url: "/:projectId/memberships",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false,
tags: [ApiDocsTags.ProjectUsers],
description: "Invite members to project",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
workspaceId: z.string().trim() projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId)
}), }),
body: z.object({ body: z.object({
members: z emails: z
.object({ .string()
orgMembershipId: z.string().trim(), .email()
workspaceEncryptedKey: z.string().trim(),
workspaceEncryptedNonce: z.string().trim()
})
.array() .array()
.min(1) .default([])
.describe(PROJECT_USERS.INVITE_MEMBER.emails)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
usernames: z
.string()
.array()
.default([])
.describe(PROJECT_USERS.INVITE_MEMBER.usernames)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase"),
roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
}), }),
response: { response: {
200: z.object({ 200: z.object({
success: z.boolean(), memberships: ProjectMembershipsSchema.array()
data: OrgMembershipsSchema.array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const data = await server.services.projectMembership.addUsersToProject({ const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
actorId: req.permission.id, const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, actor: req.permission.type,
members: req.body.members inviteeEmails: usernamesAndEmails,
orgId: req.permission.orgId,
organizationRoleSlug: OrgMembershipRole.NoAccess,
projects: [
{
id: req.params.projectId,
projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member]
}
]
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.params.workspaceId, projectId: req.params.projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.ADD_BATCH_WORKSPACE_MEMBER, type: EventType.ADD_BATCH_PROJECT_MEMBER,
metadata: data.map(({ userId }) => ({ metadata: memberships.map(({ userId, id }) => ({
userId: userId || "", userId: userId || "",
membershipId: id,
email: "" email: ""
})) }))
} }
}); });
return { data, success: true }; return { memberships };
} }
}); });
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:workspaceId/memberships/:membershipId", url: "/:projectId/memberships/:membershipId",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -268,7 +291,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.workspaceId), projectId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.projectId),
membershipId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.membershipId) membershipId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.membershipId)
}), }),
body: z.object({ body: z.object({
@@ -305,31 +328,87 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.projectId,
membershipId: req.params.membershipId, membershipId: req.params.membershipId,
roles: req.body.roles roles: req.body.roles
}); });
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: req.params.workspaceId,
// event: {
// type: EventType.UPDATE_USER_WORKSPACE_ROLE,
// metadata: {
// userId: membership.userId,
// newRole: req.body.role,
// oldRole: membership.role,
// email: ""
// }
// }
// });
return { roles }; return { roles };
} }
}); });
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:workspaceId/memberships/:membershipId", url: "/:projectId/memberships",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectUsers],
description: "Remove members from project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId)
}),
body: z.object({
emails: z
.string()
.email()
.array()
.default([])
.describe(PROJECT_USERS.REMOVE_MEMBER.emails)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
usernames: z
.string()
.array()
.default([])
.describe(PROJECT_USERS.REMOVE_MEMBER.usernames)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase")
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const memberships = await server.services.projectMembership.deleteProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
emails: req.body.emails,
usernames: req.body.usernames
});
for (const membership of memberships) {
// eslint-disable-next-line no-await-in-loop
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.projectId,
event: {
type: EventType.REMOVE_PROJECT_MEMBER,
metadata: {
userId: membership.userId,
email: ""
}
}
});
}
return { memberships };
}
});
server.route({
method: "DELETE",
url: "/:projectId/memberships/:membershipId",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -341,7 +420,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim(), projectId: z.string().trim(),
membershipId: z.string().trim() membershipId: z.string().trim()
}), }),
response: { response: {
@@ -357,15 +436,15 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId, projectId: req.params.projectId,
membershipId: req.params.membershipId membershipId: req.params.membershipId
}); });
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.REMOVE_WORKSPACE_MEMBER, type: EventType.REMOVE_PROJECT_MEMBER,
metadata: { metadata: {
userId: membership.userId, userId: membership.userId,
email: "" email: ""
@@ -378,13 +457,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:workspaceId/leave", url: "/:projectId/leave",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
workspaceId: z.string().trim() projectId: z.string().trim()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -398,7 +477,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
const membership = await server.services.projectMembership.leaveProject({ const membership = await server.services.projectMembership.leaveProject({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
projectId: req.params.workspaceId projectId: req.params.projectId
}); });
return { membership }; return { membership };
} }
File diff suppressed because it is too large Load Diff
@@ -22,20 +22,20 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTags: SecretTagsSchema.array() tags: SecretTagsSchema.array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspaceTags = await server.services.secretTag.getProjectTags({ const tags = await server.services.secretTag.getProjectTags({
actor: req.permission.type, actor: req.permission.type,
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.projectId projectId: req.params.projectId
}); });
return { workspaceTags }; return { tags };
} }
}); });
@@ -55,20 +55,20 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
// akhilmhdh: for terraform backward compatiability // akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() }) tag: SecretTagsSchema.extend({ name: z.string() })
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspaceTag = await server.services.secretTag.getTagById({ const tag = await server.services.secretTag.getTagById({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.tagId id: req.params.tagId
}); });
return { workspaceTag }; return { tag };
} }
}); });
@@ -88,13 +88,13 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
// akhilmhdh: for terraform backward compatiability // akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() }) tag: SecretTagsSchema.extend({ name: z.string() })
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspaceTag = await server.services.secretTag.getTagBySlug({ const tag = await server.services.secretTag.getTagBySlug({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -102,7 +102,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
slug: req.params.tagSlug, slug: req.params.tagSlug,
projectId: req.params.projectId projectId: req.params.projectId
}); });
return { workspaceTag }; return { tag };
} }
}); });
@@ -124,13 +124,13 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTag: SecretTagsSchema tag: SecretTagsSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspaceTag = await server.services.secretTag.createTag({ const tag = await server.services.secretTag.createTag({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -138,7 +138,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
projectId: req.params.projectId, projectId: req.params.projectId,
...req.body ...req.body
}); });
return { workspaceTag }; return { tag };
} }
}); });
@@ -161,13 +161,13 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTag: SecretTagsSchema tag: SecretTagsSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspaceTag = await server.services.secretTag.updateTag({ const tag = await server.services.secretTag.updateTag({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -175,7 +175,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
...req.body, ...req.body,
id: req.params.tagId id: req.params.tagId
}); });
return { workspaceTag }; return { tag };
} }
}); });
@@ -194,20 +194,20 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTag: SecretTagsSchema tag: SecretTagsSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspaceTag = await server.services.secretTag.deleteTag({ const tag = await server.services.secretTag.deleteTag({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.tagId id: req.params.tagId
}); });
return { workspaceTag }; return { tag };
} }
}); });
}; };
@@ -39,7 +39,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
body: z body: z
.object({ .object({
type: z.nativeEnum(WebhookType).default(WebhookType.GENERAL), type: z.nativeEnum(WebhookType).default(WebhookType.GENERAL),
workspaceId: z.string().trim(), projectId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
webhookUrl: z.string().url().trim(), webhookUrl: z.string().url().trim(),
webhookSecretKey: z.string().trim().optional(), webhookSecretKey: z.string().trim().optional(),
@@ -67,13 +67,12 @@ export const registerWebhookRouter = 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.body.workspaceId,
...req.body ...req.body
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.body.workspaceId, projectId: req.body.projectId,
event: { event: {
type: EventType.CREATE_WEBHOOK, type: EventType.CREATE_WEBHOOK,
metadata: { metadata: {
@@ -216,7 +215,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
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(),
secretPath: z secretPath: z
.string() .string()
@@ -238,7 +237,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
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 { message: "Successfully fetched webhook", webhooks }; return { message: "Successfully fetched webhook", webhooks };
} }
@@ -0,0 +1,363 @@
import { z } from "zod";
import {
GroupProjectMembershipsSchema,
GroupsSchema,
ProjectMembershipRole,
ProjectUserMembershipRolesSchema,
UsersSchema
} from "@app/db/schemas";
import { EFilterReturnedUsers } from "@app/ee/services/group/group-types";
import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
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";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/groups/:groupIdOrName",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectGroups],
description: "Add group to project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId),
groupIdOrName: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupIdOrName)
}),
body: z
.object({
role: z
.string()
.trim()
.min(1)
.default(ProjectMembershipRole.NoAccess)
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role),
roles: z
.array(
z.union([
z.object({
role: z.string(),
isTemporary: z.literal(false).default(false)
}),
z.object({
role: z.string(),
isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime()
})
])
)
.optional()
})
.refine((data) => data.role || data.roles, {
message: "Either role or roles must be present",
path: ["role", "roles"]
}),
response: {
200: z.object({
groupMembership: GroupProjectMembershipsSchema
})
}
},
handler: async (req) => {
const groupMembership = await server.services.groupProject.addGroupToProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
roles: req.body.roles || [{ role: req.body.role }],
projectId: req.params.projectId,
groupIdOrName: req.params.groupIdOrName
});
return { groupMembership };
}
});
server.route({
method: "PATCH",
url: "/:projectId/groups/:groupId",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectGroups],
description: "Update group in project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectId),
groupId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string(),
isTemporary: z.literal(false).default(false)
}),
z.object({
role: z.string(),
isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime()
})
])
)
.min(1)
.describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.roles)
}),
response: {
200: z.object({
roles: ProjectUserMembershipRolesSchema.array()
})
}
},
handler: async (req) => {
const roles = await server.services.groupProject.updateGroupInProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
groupId: req.params.groupId,
roles: req.body.roles
});
return { roles };
}
});
server.route({
method: "DELETE",
url: "/:projectId/groups/:groupId",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectGroups],
description: "Remove group from project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectId),
groupId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupId)
}),
response: {
200: z.object({
groupMembership: GroupProjectMembershipsSchema
})
}
},
handler: async (req) => {
const groupMembership = await server.services.groupProject.removeGroupFromProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
groupId: req.params.groupId,
projectId: req.params.projectId
});
return { groupMembership };
}
});
server.route({
method: "GET",
url: "/:projectId/groups",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectGroups],
description: "Return list of groups in project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectId)
}),
response: {
200: z.object({
groupMemberships: z
.object({
id: z.string(),
groupId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
group: GroupsSchema.pick({ name: true, id: true, slug: true })
})
.array()
})
}
},
handler: async (req) => {
const groupMemberships = await server.services.groupProject.listGroupsInProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId
});
return { groupMemberships };
}
});
server.route({
method: "GET",
url: "/:projectId/groups/:groupId",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectGroups],
description: "Return project group",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim(),
groupId: z.string().trim()
}),
response: {
200: z.object({
groupMembership: z.object({
id: z.string(),
groupId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
group: GroupsSchema.pick({ name: true, id: true, slug: true })
})
})
}
},
handler: async (req) => {
const groupMembership = await server.services.groupProject.getGroupInProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { groupMembership };
}
});
server.route({
method: "GET",
url: "/:projectId/groups/:groupId/users",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectGroups],
description: "Return project group users",
params: z.object({
projectId: z.string().trim().describe(GROUPS.LIST_USERS.projectId),
groupId: z.string().trim().describe(GROUPS.LIST_USERS.id)
}),
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search),
filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers)
}),
response: {
200: z.object({
users: UsersSchema.pick({
email: true,
username: true,
firstName: true,
lastName: true,
id: true
})
.merge(
z.object({
isPartOfGroup: z.boolean(),
joinedGroupAt: z.date().nullable()
})
)
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { users, totalCount } = await server.services.groupProject.listProjectGroupUsers({
id: req.params.groupId,
projectId: req.params.projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
return { users, totalCount };
}
});
};
@@ -0,0 +1,418 @@
import { z } from "zod";
import {
IdentitiesSchema,
IdentityProjectMembershipsSchema,
ProjectMembershipRole,
ProjectUserMembershipRolesSchema
} from "@app/db/schemas";
import { ApiDocsTags, ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { OrderByDirection } from "@app/lib/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";
import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Create project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim(),
identityId: z.string().trim()
}),
body: z.object({
// @depreciated
role: z.string().trim().optional().default(ProjectMembershipRole.NoAccess),
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryMode: z
.nativeEnum(ProjectUserMembershipTemporaryMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
})
])
)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.optional()
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { role, roles } = req.body;
if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" });
const identityMembership = await server.services.identityProject.createProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId,
roles: roles || [{ role }]
});
return { identityMembership };
}
});
server.route({
method: "PATCH",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Update project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(ProjectUserMembershipTemporaryMode)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}),
response: {
200: z.object({
roles: ProjectUserMembershipRolesSchema.array()
})
}
},
handler: async (req) => {
const roles = await server.services.identityProject.updateProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId,
roles: req.body.roles
});
return { roles };
}
});
server.route({
method: "DELETE",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Delete project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const identityMembership = await server.services.identityProject.deleteProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/:projectId/identity-memberships",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Return project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId)
}),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added
.default(100)
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
orderBy: z
.nativeEnum(ProjectIdentityOrderBy)
.default(ProjectIdentityOrderBy.Name)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
.optional(),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
.optional(),
search: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.search).optional()
}),
response: {
200: z.object({
identityMemberships: z
.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
})
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
limit: req.query.limit,
offset: req.query.offset,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
search: req.query.search
});
return { identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Return project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.identityProject.getProjectIdentityByIdentityId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
identityId: req.params.identityId
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/identity-memberships/:identityMembershipId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
params: z.object({
identityMembershipId: z.string().trim()
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.identityProject.getProjectIdentityByMembershipId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityMembershipId: req.params.identityMembershipId
});
return { identityMembership };
}
});
};
@@ -7,7 +7,7 @@ import { 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";
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/:projectId/memberships", url: "/:projectId/memberships",
@@ -71,7 +71,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: req.params.projectId, projectId: req.params.projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.ADD_BATCH_WORKSPACE_MEMBER, type: EventType.ADD_BATCH_PROJECT_MEMBER,
metadata: memberships.map(({ userId, id }) => ({ metadata: memberships.map(({ userId, id }) => ({
userId: userId || "", userId: userId || "",
membershipId: id, membershipId: id,
@@ -141,7 +141,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.params.projectId, projectId: req.params.projectId,
event: { event: {
type: EventType.REMOVE_WORKSPACE_MEMBER, type: EventType.REMOVE_PROJECT_MEMBER,
metadata: { metadata: {
userId: membership.userId, userId: membership.userId,
email: "" email: ""
@@ -35,7 +35,8 @@ const projectWithEnv = SanitizedProjectSchema.extend({
kmsSecretManagerKeyId: z.string().nullable().optional() kmsSecretManagerKeyId: z.string().nullable().optional()
}); });
export const registerProjectRouter = async (server: FastifyZodProvider) => { export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider) => {
// depreciated
/* Get project key */ /* Get project key */
server.route({ server.route({
method: "GET", method: "GET",
@@ -46,7 +47,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Return encrypted project key", description: "Return encrypted project key",
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.workspaceId) workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.projectId)
}), }),
response: { response: {
200: ProjectKeysSchema.merge( 200: ProjectKeysSchema.merge(
@@ -72,7 +73,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
event: { event: {
type: EventType.GET_WORKSPACE_KEY, type: EventType.GET_PROJECT_KEY,
metadata: { metadata: {
keyId: key?.id as string keyId: key?.id as string
} }
@@ -83,68 +84,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
/* Start upgrade of a project */
server.route({
method: "POST",
url: "/:projectId/upgrade",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
body: z.object({
userPrivateKey: z.string().trim()
}),
response: {
200: z.void()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.project.upgradeProject({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
projectId: req.params.projectId,
userPrivateKey: req.body.userPrivateKey
});
}
});
/* Get upgrade status of project */
server.route({
url: "/:projectId/upgrade/status",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
status: z.string().nullable()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const status = await server.services.project.getProjectUpgradeStatus({
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
actor: req.permission.type,
actorId: req.permission.id
});
return { status };
}
});
/* Create new project */ /* Create new project */
server.route({ server.route({
method: "POST", method: "POST",
@@ -186,8 +125,8 @@ export const registerProjectRouter = 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,
workspaceName: req.body.projectName, projectName: req.body.projectName,
workspaceDescription: req.body.projectDescription, projectDescription: req.body.projectDescription,
slug: req.body.slug, slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId, kmsKeyId: req.body.kmsKeyId,
template: req.body.template, template: req.body.template,
@@ -224,6 +163,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}); });
/* Delete a project by slug */ /* Delete a project by slug */
// moved to DELETE /v1/projects/slug/:slug
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:slug", url: "/:slug",
@@ -276,6 +216,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}); });
/* Get a project by slug */ /* Get a project by slug */
// moved to GET /v1/projects/slug/:slug
server.route({ server.route({
method: "GET", method: "GET",
url: "/:slug", url: "/:slug",
@@ -337,7 +278,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: SanitizedProjectSchema 200: SanitizedProjectSchema
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const project = await server.services.project.updateProject({ const project = await server.services.project.updateProject({
+15 -8
View File
@@ -1,13 +1,15 @@
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { registerGroupProjectRouter } from "./group-project-router"; import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router";
import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
import { registerIdentityOrgRouter } from "./identity-org-router"; import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router"; import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router"; import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router"; import { registerPasswordRouter } from "./password-router";
import { registerPkiTemplatesRouter } from "./pki-templates-router"; import { registerPkiTemplatesRouter } from "./pki-templates-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerSecretFolderRouter } from "./secret-folder-router";
import { registerProjectRouter } from "./project-router"; import { registerSecretImportRouter } from "./secret-import-router";
import { registerServiceTokenRouter } from "./service-token-router"; import { registerServiceTokenRouter } from "./service-token-router";
import { registerUserRouter } from "./user-router"; import { registerUserRouter } from "./user-router";
@@ -32,12 +34,17 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
}, },
{ prefix: "/organizations" } { prefix: "/organizations" }
); );
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" });
// moved to v1/projects
await server.register( await server.register(
async (projectServer) => { async (projectServer) => {
await projectServer.register(registerProjectRouter); await projectServer.register(registerDeprecatedProjectRouter);
await projectServer.register(registerIdentityProjectRouter); await projectServer.register(registerDeprecatedIdentityProjectRouter);
await projectServer.register(registerGroupProjectRouter); await projectServer.register(registerDeprecatedGroupProjectRouter);
await projectServer.register(registerProjectMembershipRouter); await projectServer.register(registerDeprecatedProjectMembershipRouter);
}, },
{ prefix: "/workspace" } { prefix: "/workspace" }
); );
@@ -28,7 +28,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
} }
], ],
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId), projectId: z.string().trim().describe(FOLDERS.CREATE.projectId),
environment: z.string().trim().describe(FOLDERS.CREATE.environment), environment: z.string().trim().describe(FOLDERS.CREATE.environment),
name: z name: z
.string() .string()
@@ -43,17 +43,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/") .default("/")
.transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.path) .describe(FOLDERS.CREATE.path),
.optional(),
// backward compatibility with cli
directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description) description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
}), }),
response: { response: {
@@ -66,27 +56,24 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.createFolder({ const folder = await server.services.folder.createFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectId: req.body.workspaceId,
path,
description: req.body.description description: req.body.description
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.body.workspaceId, projectId: req.body.projectId,
event: { event: {
type: EventType.CREATE_FOLDER, type: EventType.CREATE_FOLDER,
metadata: { metadata: {
environment: req.body.environment, environment: req.body.environment,
folderId: folder.id, folderId: folder.id,
folderName: folder.name, folderName: folder.name,
folderPath: path, folderPath: req.body.path,
...(req.body.description ? { description: req.body.description } : {}) ...(req.body.description ? { description: req.body.description } : {})
} }
} }
@@ -115,7 +102,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
folderId: z.string().describe(FOLDERS.UPDATE.folderId) folderId: z.string().describe(FOLDERS.UPDATE.folderId)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId), projectId: z.string().trim().describe(FOLDERS.UPDATE.projectId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment), environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z name: z
.string() .string()
@@ -130,17 +117,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/") .default("/")
.transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path) .describe(FOLDERS.UPDATE.path),
.optional(),
// backward compatibility with cli
directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.directory)
.optional(),
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description) description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
}), }),
response: { response: {
@@ -153,26 +130,23 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory || "/";
const { folder, old } = await server.services.folder.updateFolder({ const { folder, old } = await server.services.folder.updateFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectId: req.body.workspaceId, id: req.params.folderId
id: req.params.folderId,
path
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.body.workspaceId, projectId: req.body.projectId,
event: { event: {
type: EventType.UPDATE_FOLDER, type: EventType.UPDATE_FOLDER,
metadata: { metadata: {
environment: req.body.environment, environment: req.body.environment,
folderId: folder.id, folderId: folder.id,
folderPath: path, folderPath: req.body.path,
newFolderName: folder.name, newFolderName: folder.name,
oldFolderName: old.name oldFolderName: old.name
} }
@@ -198,7 +172,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
} }
], ],
body: z.object({ body: z.object({
projectSlug: z.string().trim().describe(FOLDERS.UPDATE.projectSlug), projectId: z.string().trim().describe(FOLDERS.UPDATE.projectId),
folders: z folders: z
.object({ .object({
id: z.string().describe(FOLDERS.UPDATE.folderId), id: z.string().describe(FOLDERS.UPDATE.folderId),
@@ -281,7 +255,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
folderIdOrName: z.string().describe(FOLDERS.DELETE.folderIdOrName) folderIdOrName: z.string().describe(FOLDERS.DELETE.folderIdOrName)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId), projectId: z.string().trim().describe(FOLDERS.DELETE.projectId),
environment: z.string().trim().describe(FOLDERS.DELETE.environment), environment: z.string().trim().describe(FOLDERS.DELETE.environment),
path: z path: z
.string() .string()
@@ -290,16 +264,6 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.path) .describe(FOLDERS.DELETE.path)
.optional(),
// keep this here as cli need directory
directory: z
.string()
.trim()
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.directory)
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -309,26 +273,23 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.body.path || req.body.directory || "/";
const folder = await server.services.folder.deleteFolder({ const folder = await server.services.folder.deleteFolder({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectId: req.body.workspaceId, idOrName: req.params.folderIdOrName
idOrName: req.params.folderIdOrName,
path
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.body.workspaceId, projectId: req.body.projectId,
event: { event: {
type: EventType.DELETE_FOLDER, type: EventType.DELETE_FOLDER,
metadata: { metadata: {
environment: req.body.environment, environment: req.body.environment,
folderId: folder.id, folderId: folder.id,
folderPath: path, folderPath: req.body.path,
folderName: folder.name folderName: folder.name
} }
} }
@@ -353,7 +314,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
} }
], ],
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId), projectId: z.string().trim().describe(FOLDERS.LIST.projectId),
environment: z.string().trim().describe(FOLDERS.LIST.environment), environment: z.string().trim().describe(FOLDERS.LIST.environment),
lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified), lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified),
path: z path: z
@@ -361,16 +322,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.trim() .trim()
.transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.LIST.path) .describe(FOLDERS.LIST.path),
.optional(),
// backward compatibility with cli
directory: z
.string()
.trim()
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.LIST.directory)
.optional(),
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
}), }),
response: { response: {
@@ -383,15 +335,12 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const path = req.query.path || req.query.directory || "/";
const folders = await server.services.folder.getFolders({ const folders = await server.services.folder.getFolders({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.query, ...req.query
projectId: req.query.workspaceId,
path
}); });
return { folders }; return { folders };
} }
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; import { SecretImportsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, SECRET_IMPORTS } from "@app/lib/api-docs"; import { ApiDocsTags, SECRET_IMPORTS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
@@ -27,7 +27,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
} }
], ],
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.CREATE.workspaceId), projectId: z.string().trim().describe(SECRET_IMPORTS.CREATE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.environment), environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.path), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.path),
import: z.object({ import: z.object({
@@ -55,13 +55,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
projectId: req.body.workspaceId, projectId: req.body.projectId,
data: req.body.import data: req.body.import
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.body.workspaceId, projectId: req.body.projectId,
event: { event: {
type: EventType.CREATE_SECRET_IMPORT, type: EventType.CREATE_SECRET_IMPORT,
metadata: { metadata: {
@@ -97,7 +97,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId) secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.workspaceId), projectId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment), environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path),
import: z.object({ import: z.object({
@@ -131,13 +131,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.secretImportId, id: req.params.secretImportId,
...req.body, ...req.body,
projectId: req.body.workspaceId, projectId: req.body.projectId,
data: req.body.import data: req.body.import
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.body.workspaceId, projectId: req.body.projectId,
event: { event: {
type: EventType.UPDATE_SECRET_IMPORT, type: EventType.UPDATE_SECRET_IMPORT,
metadata: { metadata: {
@@ -173,7 +173,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
secretImportId: z.string().trim().describe(SECRET_IMPORTS.DELETE.secretImportId) secretImportId: z.string().trim().describe(SECRET_IMPORTS.DELETE.secretImportId)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.DELETE.workspaceId), projectId: z.string().trim().describe(SECRET_IMPORTS.DELETE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.DELETE.environment), environment: z.string().trim().describe(SECRET_IMPORTS.DELETE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.DELETE.path) path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.DELETE.path)
}), }),
@@ -197,12 +197,12 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.secretImportId, id: req.params.secretImportId,
...req.body, ...req.body,
projectId: req.body.workspaceId projectId: req.body.projectId
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.body.workspaceId, projectId: req.body.projectId,
event: { event: {
type: EventType.DELETE_SECRET_IMPORT, type: EventType.DELETE_SECRET_IMPORT,
metadata: { metadata: {
@@ -236,7 +236,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId) secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.workspaceId), projectId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment), environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path) path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path)
}), }),
@@ -255,7 +255,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.secretImportId, id: req.params.secretImportId,
...req.body, ...req.body,
projectId: req.body.workspaceId projectId: req.body.projectId
}); });
return { message }; return { message };
@@ -278,7 +278,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
} }
], ],
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim().describe(SECRET_IMPORTS.LIST.workspaceId), projectId: z.string().trim().describe(SECRET_IMPORTS.LIST.projectId),
environment: z.string().trim().describe(SECRET_IMPORTS.LIST.environment), environment: z.string().trim().describe(SECRET_IMPORTS.LIST.environment),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.LIST.path) path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.LIST.path)
}), }),
@@ -301,12 +301,12 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
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
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.query.workspaceId, projectId: req.query.projectId,
event: { event: {
type: EventType.GET_SECRET_IMPORTS, type: EventType.GET_SECRET_IMPORTS,
metadata: { metadata: {
@@ -386,55 +386,11 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
config: { config: {
rateLimit: secretsLimit rateLimit: secretsLimit
}, },
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
secrets: z
.object({
secretPath: z.string(),
environment: z.string(),
environmentInfo: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
folderId: z.string().optional(),
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const importedSecrets = await server.services.secretImport.getSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
});
return { secrets: importedSecrets };
}
});
server.route({
url: "/secrets/raw",
method: "GET",
config: {
rateLimit: secretsLimit
},
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.SecretImports], tags: [ApiDocsTags.SecretImports],
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim(), projectId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash) path: z.string().trim().default("/").transform(removeTrailingSlash)
}), }),
@@ -463,8 +419,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.query, ...req.query
projectId: req.query.workspaceId
}); });
return { secrets: importedSecrets }; return { secrets: importedSecrets };
} }
@@ -39,7 +39,7 @@ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReference
children: z.lazy(() => SecretReferenceNodeTree.array()) children: z.lazy(() => SecretReferenceNodeTree.array())
}); });
export const registerSecretRouter = async (server: FastifyZodProvider) => { export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/tags/:secretName", url: "/tags/:secretName",
@@ -230,7 +230,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}) })
.describe(RAW_SECRETS.LIST.metadataFilter), .describe(RAW_SECRETS.LIST.metadataFilter),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.projectId),
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
@@ -298,7 +298,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceId = projectId; workspaceId = projectId;
} }
if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing project id or environment" });
const { secrets, imports } = await server.services.secret.getSecretsRaw({ const { secrets, imports } = await server.services.secret.getSecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
@@ -336,7 +336,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId, projectId: workspaceId,
environment, environment,
secretPath: req.query.secretPath, secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -405,7 +405,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName)
}), }),
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.projectId),
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceSlug), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
@@ -495,7 +495,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: secret.workspace, projectId: secret.workspace,
environment, environment,
secretPath: req.query.secretPath, secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -526,7 +526,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
secretPath: z secretPath: z
@@ -627,7 +627,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id, secretId: secret.id,
secretKey: req.params.secretName, secretKey: req.params.secretName,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: req.body.secretMetadata secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
} }
} }
}); });
@@ -638,7 +639,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: projectId, projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -669,7 +670,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
secretValue: z secretValue: z
@@ -780,7 +781,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id, secretId: secret.id,
secretKey: req.params.secretName, secretKey: req.params.secretName,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: req.body.secretMetadata secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
} }
} }
}); });
@@ -791,7 +793,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: projectId, projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -821,7 +823,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName)
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
secretPath: z secretPath: z
@@ -909,7 +911,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: projectId, projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1017,7 +1019,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: req.query.workspaceId, projectId: req.query.workspaceId,
environment: req.query.environment, environment: req.query.environment,
secretPath: req.query.secretPath, secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1097,7 +1099,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req.query.workspaceId, projectId: req.query.workspaceId,
environment: req.query.environment, environment: req.query.environment,
secretPath: req.query.secretPath, secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1272,7 +1274,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req.body.workspaceId, projectId: req.body.workspaceId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1466,7 +1468,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req.body.workspaceId, projectId: req.body.workspaceId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1594,7 +1596,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req.body.workspaceId, projectId: req.body.workspaceId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1781,7 +1783,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: req.body.workspaceId, projectId: req.body.workspaceId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -1914,7 +1916,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: req.body.workspaceId, projectId: req.body.workspaceId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -2039,7 +2041,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: req.body.workspaceId, projectId: req.body.workspaceId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -2067,7 +2069,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
], ],
body: z.object({ body: z.object({
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectId),
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
secretPath: z secretPath: z
.string() .string()
@@ -2154,7 +2156,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id, secretId: secret.id,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
@@ -2166,7 +2169,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: secrets[0].workspace, projectId: secrets[0].workspace,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -2194,7 +2197,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
], ],
body: z.object({ body: z.object({
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId),
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
secretPath: z secretPath: z
.string() .string()
@@ -2288,7 +2291,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
return { approval: secretOperation.approval }; return { approval: secretOperation.approval };
} }
const { secrets } = secretOperation; const { secrets } = secretOperation;
const secretMetadataMap = new Map( const secretMetadataMap = new Map(
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
); );
@@ -2308,7 +2310,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: secret.secretPath, secretPath: secret.secretPath,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
@@ -2328,7 +2331,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: secret.secretPath, secretPath: secret.secretPath,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
@@ -2341,7 +2345,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: secrets[0].workspace, projectId: secrets[0].workspace,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -2369,7 +2373,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
], ],
body: z.object({ body: z.object({
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId),
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
secretPath: z secretPath: z
.string() .string()
@@ -2459,7 +2463,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
workspaceId: secrets[0].workspace, projectId: secrets[0].workspace,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -2469,95 +2473,4 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
return { secrets }; return { secrets };
} }
}); });
server.route({
method: "GET",
url: "/raw/:secretName/secret-reference-tree",
config: {
rateLimit: secretsLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Secrets],
description: "Get secret reference tree",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName)
}),
querystring: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId),
environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath)
}),
response: {
200: z.object({
tree: SecretReferenceNodeTree,
value: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretName } = req.params;
const { secretPath, environment, workspaceId } = req.query;
const { tree, value } = await server.services.secret.getSecretReferenceTree({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: workspaceId,
secretName,
secretPath,
environment
});
return { tree, value };
}
});
server.route({
method: "POST",
url: "/backfill-secret-references",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Backfill secret references",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId } = req.body;
const message = await server.services.secret.backfillSecretReferences({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId
});
return message;
}
});
}; };
+2 -4
View File
@@ -1,7 +1,6 @@
import { registerDeprecatedSecretRouter } from "./deprecated-secret-router";
import { registerExternalMigrationRouter } from "./external-migration-router"; import { registerExternalMigrationRouter } from "./external-migration-router";
import { registerLoginRouter } from "./login-router"; import { registerLoginRouter } from "./login-router";
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
import { registerSecretRouter } from "./secret-router";
import { registerSignupRouter } from "./signup-router"; import { registerSignupRouter } from "./signup-router";
import { registerUserRouter } from "./user-router"; import { registerUserRouter } from "./user-router";
@@ -9,7 +8,6 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerSignupRouter, { prefix: "/signup" }); await server.register(registerSignupRouter, { prefix: "/signup" });
await server.register(registerLoginRouter, { prefix: "/auth" }); await server.register(registerLoginRouter, { prefix: "/auth" });
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerDeprecatedSecretRouter, { prefix: "/secrets" });
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" }); await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" });
}; };
@@ -1,109 +0,0 @@
import { z } from "zod";
import { SecretsSchema } from "@app/db/schemas";
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 registerSecretBlindIndexRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:projectId/secrets/blind-index-status",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.boolean()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const count = await server.services.secretBlindIndex.getSecretBlindIndexStatus({
projectId: req.params.projectId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
return count === 0;
}
});
server.route({
method: "GET",
url: "/:projectId/secrets",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true })
.merge(
z.object({
environment: z.string(),
workspace: z.string()
})
)
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secrets = await server.services.secretBlindIndex.getProjectSecrets({
projectId: req.params.projectId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
return { secrets };
}
});
server.route({
method: "POST",
url: "/:projectId/secrets/names",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
body: z.object({
secretsToUpdate: z
.object({
secretName: z.string().trim(),
secretId: z.string().trim()
})
.array()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.secretBlindIndex.updateProjectSecretName({
projectId: req.params.projectId,
secretsToUpdate: req.body.secretsToUpdate,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
return { message: "Successfully named workspace secrets" };
}
});
};
+5
View File
@@ -0,0 +1,5 @@
import { registerSecretRouter } from "./secret-router";
export const registerV4Routes = async (server: FastifyZodProvider) => {
await server.register(registerSecretRouter, { prefix: "/secrets" });
};
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,115 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName, TAppConnections } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
import { transformUsageToProjects } from "@app/services/app-connection/app-connection-fns";
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>; export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
type AppConnectionFindFilter = Parameters<typeof buildFindFilter<TAppConnections>>[0];
export const appConnectionDALFactory = (db: TDbClient) => { export const appConnectionDALFactory = (db: TDbClient) => {
const appConnectionOrm = ormify(db, TableName.AppConnection); const appConnectionOrm = ormify(db, TableName.AppConnection);
return { ...appConnectionOrm }; const findWithProjectDetails = async (filter: AppConnectionFindFilter, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.AppConnection)
.leftJoin(TableName.Project, `${TableName.AppConnection}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.AppConnection))
.select(
// project
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
);
if (filter) {
/* eslint-disable @typescript-eslint/no-misused-promises */
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.AppConnection, filter)));
}
const connections = await query;
return connections.map(({ projectName, projectSlug, projectType, projectId, ...connection }) => ({
...connection,
projectId,
project: projectId
? {
name: projectName,
type: projectType,
slug: projectSlug,
id: projectId
}
: null
}));
};
const findAppConnectionUsageById = async (connectionId: string, tx?: Knex) => {
const secretSyncs = await (tx || db.replicaNode())(TableName.SecretSync)
.where(`${TableName.SecretSync}.connectionId`, connectionId)
.join(TableName.Project, `${TableName.SecretSync}.projectId`, `${TableName.Project}.id`)
.select(
db.ref("name").withSchema(TableName.SecretSync),
db.ref("id").withSchema(TableName.SecretSync),
db.ref("projectId").withSchema(TableName.SecretSync),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project)
);
const secretRotations = await (tx || db.replicaNode())(TableName.SecretRotationV2)
.where(`${TableName.SecretRotationV2}.connectionId`, connectionId)
.join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
db.ref("name").withSchema(TableName.SecretRotationV2),
db.ref("id").withSchema(TableName.SecretRotationV2),
db.ref("id").as("projectId").withSchema(TableName.Project),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project)
);
const externalCas = await (tx || db.replicaNode())(TableName.ExternalCertificateAuthority)
.where(`${TableName.ExternalCertificateAuthority}.appConnectionId`, connectionId)
.orWhere(`${TableName.ExternalCertificateAuthority}.dnsAppConnectionId`, connectionId)
.join(
TableName.CertificateAuthority,
`${TableName.ExternalCertificateAuthority}.caId`,
`${TableName.CertificateAuthority}.id`
)
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
.select(
db.ref("name").withSchema(TableName.CertificateAuthority),
db.ref("id").withSchema(TableName.ExternalCertificateAuthority),
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority),
db.ref("dnsAppConnectionId").withSchema(TableName.ExternalCertificateAuthority),
db.ref("id").as("projectId").withSchema(TableName.Project),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project)
);
const dataSources = await (tx || db.replicaNode())(TableName.SecretScanningDataSource)
.where(`${TableName.SecretScanningDataSource}.connectionId`, connectionId)
.join(TableName.Project, `${TableName.SecretScanningDataSource}.projectId`, `${TableName.Project}.id`)
.select(
db.ref("name").withSchema(TableName.SecretScanningDataSource),
db.ref("id").withSchema(TableName.SecretScanningDataSource),
db.ref("id").as("projectId").withSchema(TableName.Project),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project)
);
return transformUsageToProjects({
secretSyncs,
secretRotations,
dataSources,
externalCas
});
};
return { ...appConnectionOrm, findAppConnectionUsageById, findWithProjectDetails };
}; };
@@ -1,3 +1,4 @@
import { ProjectType } from "@app/db/schemas";
import { TAppConnections } from "@app/db/schemas/app-connections"; import { TAppConnections } from "@app/db/schemas/app-connections";
import { import {
getOCIConnectionListItem, getOCIConnectionListItem,
@@ -8,6 +9,8 @@ import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee
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 { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
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 { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps"; import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps";
@@ -16,6 +19,7 @@ import {
validateSqlConnectionCredentials validateSqlConnectionCredentials
} from "@app/services/app-connection/shared/sql"; } from "@app/services/app-connection/shared/sql";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { import {
getOnePassConnectionListItem, getOnePassConnectionListItem,
@@ -133,7 +137,22 @@ import {
} from "./windmill"; } from "./windmill";
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix"; import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix";
export const listAppConnectionOptions = () => { const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [value, key])
);
const SECRET_ROTATION_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_ROTATION_CONNECTION_MAP).map(([key, value]) => [value, key])
);
const SECRET_SCANNING_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP).map(([key, value]) => [value, key])
);
// scott: ideally this would be derived from a utilized map like the above
const PKI_APP_CONNECTIONS = [AppConnection.AWS, AppConnection.Cloudflare, AppConnection.AzureADCS];
export const listAppConnectionOptions = (projectType?: ProjectType) => {
return [ return [
getAwsConnectionListItem(), getAwsConnectionListItem(),
getGitHubConnectionListItem(), getGitHubConnectionListItem(),
@@ -173,22 +192,51 @@ export const listAppConnectionOptions = () => {
getDigitalOceanConnectionListItem(), getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(), getNetlifyConnectionListItem(),
getOktaConnectionListItem() getOktaConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ]
.filter((option) => {
switch (projectType) {
case ProjectType.SecretManager:
return (
Boolean(SECRET_SYNC_APP_CONNECTION_MAP[option.app]) ||
Boolean(SECRET_ROTATION_APP_CONNECTION_MAP[option.app])
);
case ProjectType.SecretScanning:
return Boolean(SECRET_SCANNING_APP_CONNECTION_MAP[option.app]);
case ProjectType.CertificateManager:
return PKI_APP_CONNECTIONS.includes(option.app);
case ProjectType.KMS:
return false;
case ProjectType.SSH:
return false;
default:
return true;
}
})
.sort((a, b) => a.name.localeCompare(b.name));
}; };
export const encryptAppConnectionCredentials = async ({ export const encryptAppConnectionCredentials = async ({
orgId, orgId,
credentials, credentials,
kmsService kmsService,
projectId
}: { }: {
orgId: string; orgId: string;
credentials: TAppConnection["credentials"]; credentials: TAppConnection["credentials"];
kmsService: TAppConnectionServiceFactoryDep["kmsService"]; kmsService: TAppConnectionServiceFactoryDep["kmsService"];
projectId: string | null | undefined;
}) => { }) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor } = await kmsService.createCipherPairWithDataKey(
type: KmsDataKey.Organization, projectId
orgId ? {
}); type: KmsDataKey.SecretManager,
projectId
}
: {
type: KmsDataKey.Organization,
orgId
}
);
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
plainText: Buffer.from(JSON.stringify(credentials)) plainText: Buffer.from(JSON.stringify(credentials))
@@ -200,16 +248,22 @@ export const encryptAppConnectionCredentials = async ({
export const decryptAppConnectionCredentials = async ({ export const decryptAppConnectionCredentials = async ({
orgId, orgId,
encryptedCredentials, encryptedCredentials,
kmsService kmsService,
projectId
}: { }: {
orgId: string; orgId: string;
encryptedCredentials: Buffer; encryptedCredentials: Buffer;
kmsService: TAppConnectionServiceFactoryDep["kmsService"]; kmsService: TAppConnectionServiceFactoryDep["kmsService"];
projectId: string | null | undefined;
}) => { }) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey(
type: KmsDataKey.Organization, projectId
orgId ? { type: KmsDataKey.SecretManager, projectId }
}); : {
type: KmsDataKey.Organization,
orgId
}
);
const decryptedPlainTextBlob = decryptor({ const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedCredentials cipherTextBlob: encryptedCredentials
@@ -343,6 +397,7 @@ export const decryptAppConnection = async (
credentials: await decryptAppConnectionCredentials({ credentials: await decryptAppConnectionCredentials({
encryptedCredentials: appConnection.encryptedCredentials, encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService kmsService
}), }),
credentialsHash: crypto.nativeCrypto.createHash("sha256").update(appConnection.encryptedCredentials).digest("hex") credentialsHash: crypto.nativeCrypto.createHash("sha256").update(appConnection.encryptedCredentials).digest("hex")
@@ -413,3 +468,73 @@ export const enterpriseAppCheck = async (
}); });
} }
}; };
type Resource = {
name: string;
id: string;
projectId: string;
projectName: string;
projectSlug: string;
projectType: string;
};
type UsageData = {
secretSyncs: Resource[];
secretRotations: Resource[];
dataSources: Resource[];
externalCas: Resource[];
};
type ResourceSummary = {
name: string;
id: string;
};
type ProjectWithResources = {
id: string;
name: string;
slug: string;
type: ProjectType;
resources: {
secretSyncs: ResourceSummary[];
secretRotations: ResourceSummary[];
dataSources: ResourceSummary[];
externalCas: (ResourceSummary & { appConnectionId?: string; dnsAppConnectionId?: string })[];
};
};
export const transformUsageToProjects = (data: UsageData): ProjectWithResources[] => {
const projectMap = new Map<string, ProjectWithResources>();
Object.entries(data).forEach(([resourceType, resources]) => {
resources.forEach((resource) => {
const { projectId, projectName, projectSlug, projectType, name, id, ...rest } = resource;
const projectKey = projectId;
if (!projectMap.has(projectKey)) {
projectMap.set(projectKey, {
id: projectId,
name: projectName,
slug: projectSlug,
type: projectType as ProjectType,
resources: {
secretSyncs: [],
secretRotations: [],
dataSources: [],
externalCas: []
}
});
}
const project = projectMap.get(projectKey)!;
project.resources[resourceType as keyof ProjectWithResources["resources"]].push({
name,
id,
...rest
});
});
});
return Array.from(projectMap.values());
};
@@ -13,7 +13,15 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
app: true, app: true,
method: true method: true
}).extend({ }).extend({
credentialsHash: z.string().optional() credentialsHash: z.string().optional(),
project: z
.object({
name: z.string(),
id: z.string(),
type: z.string(),
slug: z.string()
})
.nullish()
}); });
export const GenericCreateAppConnectionFieldsSchema = ( export const GenericCreateAppConnectionFieldsSchema = (
@@ -28,6 +36,7 @@ export const GenericCreateAppConnectionFieldsSchema = (
.max(256, "Description cannot exceed 256 characters") .max(256, "Description cannot exceed 256 characters")
.nullish() .nullish()
.describe(AppConnections.CREATE(app).description), .describe(AppConnections.CREATE(app).description),
projectId: z.string().optional().describe(AppConnections.CREATE(app).projectId),
isPlatformManagedCredentials: supportsPlatformManagedCredentials isPlatformManagedCredentials: supportsPlatformManagedCredentials
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials) ? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
: z : z
@@ -1,5 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TAppConnections } from "@app/db/schemas";
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
@@ -14,6 +15,10 @@ import {
OrgPermissionSubjects OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission"; } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionAppConnectionActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { DatabaseErrorCode } from "@app/lib/error-codes"; import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
@@ -27,9 +32,8 @@ import {
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
validateAppConnectionCredentials validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns"; } from "@app/services/app-connection/app-connection-fns";
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
import { githubRadarConnectionService } from "@app/services/app-connection/github-radar/github-radar-connection-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { ValidateOnePassConnectionCredentialsSchema } from "./1password"; import { ValidateOnePassConnectionCredentialsSchema } from "./1password";
import { onePassConnectionService } from "./1password/1password-connection-service"; import { onePassConnectionService } from "./1password/1password-connection-service";
@@ -41,10 +45,13 @@ import {
TAppConnectionConfig, TAppConnectionConfig,
TAppConnectionRaw, TAppConnectionRaw,
TCreateAppConnectionDTO, TCreateAppConnectionDTO,
TGetAppConnectionByNameDTO,
TUpdateAppConnectionDTO, TUpdateAppConnectionDTO,
TValidateAppConnectionCredentialsSchema TValidateAppConnectionCredentialsSchema,
TValidateAppConnectionUsageByIdDTO
} from "./app-connection-types"; } from "./app-connection-types";
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0"; import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
import { auth0ConnectionService } from "./auth0/auth0-connection-service";
import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service"; import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas"; import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas";
@@ -73,6 +80,7 @@ import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service"; import { githubConnectionService } from "./github/github-connection-service";
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar"; import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
import { githubRadarConnectionService } from "./github-radar/github-radar-connection-service";
import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab"; import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab";
import { gitlabConnectionService } from "./gitlab/gitlab-connection-service"; import { gitlabConnectionService } from "./gitlab/gitlab-connection-service";
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault"; import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
@@ -108,13 +116,14 @@ import { zabbixConnectionService } from "./zabbix/zabbix-connection-service";
export type TAppConnectionServiceFactoryDep = { export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">; gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
gatewayDAL: Pick<TGatewayDALFactory, "find">; gatewayDAL: Pick<TGatewayDALFactory, "find">;
gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">; gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findProjectById">;
}; };
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>; export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -168,29 +177,64 @@ export const appConnectionServiceFactory = ({
gatewayService, gatewayService,
gatewayV2Service, gatewayV2Service,
gatewayDAL, gatewayDAL,
gatewayV2DAL gatewayV2DAL,
projectDAL
}: TAppConnectionServiceFactoryDep) => { }: TAppConnectionServiceFactoryDep) => {
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const listAppConnections = async (actor: OrgServiceActor, app?: AppConnection, projectId?: string) => {
const { permission } = await permissionService.getOrgPermission( let appConnections: TAppConnections[];
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan( if (projectId) {
OrgPermissionAppConnectionActions.Read, const { permission } = await permissionService.getProjectPermission({
OrgPermissionSubjects.AppConnections actor: actor.type,
); actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
const appConnections = await appConnectionDAL.find( ForbiddenError.from(permission).throwUnlessCan(
app ProjectPermissionAppConnectionActions.Read,
? { orgId: actor.orgId, app } ProjectPermissionSub.AppConnections
: { );
orgId: actor.orgId
} appConnections = (
); await appConnectionDAL.findWithProjectDetails({
projectId,
...(app ? { app } : {})
})
).filter((appConnection) =>
permission.can(
ProjectPermissionAppConnectionActions.Read,
subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id })
)
);
} else {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections
);
appConnections = (
await appConnectionDAL.findWithProjectDetails({
orgId: actor.orgId,
...(app ? { app } : {})
})
).filter((appConnection) =>
permission.can(
OrgPermissionAppConnectionActions.Read,
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id })
)
);
}
return Promise.all( return Promise.all(
appConnections appConnections
@@ -204,18 +248,34 @@ export const appConnectionServiceFactory = ({
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
appConnection.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, ProjectPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections subject(ProjectPermissionSub.AppConnections, { connectionId })
); );
} else {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read,
subject(OrgPermissionSubjects.AppConnections, { connectionId })
);
}
if (appConnection.app !== app) if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` }); throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
@@ -223,24 +283,49 @@ export const appConnectionServiceFactory = ({
return decryptAppConnection(appConnection, kmsService); return decryptAppConnection(appConnection, kmsService);
}; };
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => { const findAppConnectionByName = async (
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId }); app: AppConnection,
{ connectionName, projectId }: TGetAppConnectionByNameDTO,
actor: OrgServiceActor
) => {
const appConnection = await appConnectionDAL.findOne({
name: connectionName,
...(projectId ? { projectId } : { orgId: actor.orgId, projectId: null })
});
if (!appConnection) if (!appConnection)
throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` }); throw new NotFoundError({
message: `Could not find App Connection with name ${connectionName} in ${projectId ? "project" : "organization"} scope.`
});
const { permission } = await permissionService.getOrgPermission( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
appConnection.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, ProjectPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id })
); );
} else {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read,
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id })
);
}
if (appConnection.app !== app) if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` }); throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
@@ -249,10 +334,10 @@ export const appConnectionServiceFactory = ({
}; };
const createAppConnection = async ( const createAppConnection = async (
{ method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO, { method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const { permission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, actor.orgId,
@@ -260,13 +345,33 @@ export const appConnectionServiceFactory = ({
actor.orgId actor.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( if (projectId) {
OrgPermissionAppConnectionActions.Create, const project = await projectDAL.findProjectById(projectId);
OrgPermissionSubjects.AppConnections
); if (!project) throw new BadRequestError({ message: `Could not find project with ID ${projectId}` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
} else {
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
);
}
if (gatewayId) { if (gatewayId) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways, OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
); );
@@ -304,7 +409,8 @@ export const appConnectionServiceFactory = ({
const encryptedCredentials = await encryptAppConnectionCredentials({ const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: connectionCredentials, credentials: connectionCredentials,
orgId: actor.orgId, orgId: actor.orgId,
kmsService kmsService,
projectId
}); });
return appConnectionDAL.create({ return appConnectionDAL.create({
@@ -313,6 +419,7 @@ export const appConnectionServiceFactory = ({
method, method,
app, app,
gatewayId, gatewayId,
projectId,
...params ...params
}); });
}; };
@@ -365,7 +472,7 @@ export const appConnectionServiceFactory = ({
"Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections."
); );
const { permission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, actor.orgId,
@@ -373,13 +480,29 @@ export const appConnectionServiceFactory = ({
appConnection.orgId appConnection.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( if (appConnection.projectId) {
OrgPermissionAppConnectionActions.Edit, const { permission } = await permissionService.getProjectPermission({
OrgPermissionSubjects.AppConnections actor: actor.type,
); actorId: actor.id,
projectId: appConnection.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
if (gatewayId !== appConnection.gatewayId) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionAppConnectionActions.Edit,
subject(ProjectPermissionSub.AppConnections, { connectionId })
);
} else {
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionAppConnectionActions.Edit,
subject(OrgPermissionSubjects.AppConnections, { connectionId })
);
}
if (gatewayId !== undefined && gatewayId !== appConnection.gatewayId) {
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways, OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
); );
@@ -441,7 +564,8 @@ export const appConnectionServiceFactory = ({
? await encryptAppConnectionCredentials({ ? await encryptAppConnectionCredentials({
credentials: connectionCredentials, credentials: connectionCredentials,
orgId: actor.orgId, orgId: actor.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}) })
: undefined; : undefined;
@@ -491,18 +615,34 @@ export const appConnectionServiceFactory = ({
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
appConnection.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Delete, ProjectPermissionAppConnectionActions.Delete,
OrgPermissionSubjects.AppConnections subject(ProjectPermissionSub.AppConnections, { connectionId })
); );
} else {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Delete,
subject(OrgPermissionSubjects.AppConnections, { connectionId })
);
}
if (appConnection.app !== app) if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` }); throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
@@ -544,18 +684,34 @@ export const appConnectionServiceFactory = ({
"Failed to connect app due to plan restriction. Upgrade plan to access enterprise app connections." "Failed to connect app due to plan restriction. Upgrade plan to access enterprise app connections."
); );
const { permission: orgPermission } = await permissionService.getOrgPermission( if (appConnection.projectId) {
actor.type, const { permission } = await permissionService.getProjectPermission({
actor.id, actor: actor.type,
appConnection.orgId, actorId: actor.id,
actor.authMethod, projectId: appConnection.projectId,
actor.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(orgPermission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Connect, ProjectPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id }) subject(ProjectPermissionSub.AppConnections, { connectionId })
); );
} else {
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
appConnection.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId })
);
}
if (appConnection.app !== app) if (appConnection.app !== app)
throw new BadRequestError({ throw new BadRequestError({
@@ -569,7 +725,23 @@ export const appConnectionServiceFactory = ({
return connection as T; return connection as T;
}; };
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor) => { const validateAppConnectionUsageById = async (
app: AppConnection,
{ connectionId, projectId }: TValidateAppConnectionUsageByIdDTO,
actor: OrgServiceActor
) => {
const appConnection = await connectAppConnectionById(app, connectionId, actor);
if (appConnection.projectId && appConnection.projectId !== projectId) {
throw new BadRequestError({
message: `You cannot connect project App Connection with ID "${appConnection.id}" from project with ID "${appConnection.projectId}" to project with ID "${projectId}"`
});
}
return appConnection;
};
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => {
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -578,28 +750,89 @@ export const appConnectionServiceFactory = ({
actor.orgId actor.orgId
); );
const appConnections = await appConnectionDAL.find({ app, orgId: actor.orgId }); let availableProjectConnections: TAppConnections[] = [];
const availableConnections = appConnections.filter((connection) => if (projectId) {
const project = await projectDAL.findProjectById(projectId);
if (!project) throw new BadRequestError({ message: `Could not find project with ID ${projectId}` });
const { permission: projectPermission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionAppConnectionActions.Connect,
ProjectPermissionSub.AppConnections
);
const projectAppConnections = await appConnectionDAL.find({ app, projectId });
availableProjectConnections = projectAppConnections.filter((connection) =>
projectPermission.can(
ProjectPermissionAppConnectionActions.Connect,
subject(ProjectPermissionSub.AppConnections, { connectionId: connection.id })
)
);
}
const orgAppConnections = await appConnectionDAL.find({ app, orgId: actor.orgId, projectId: null });
const availableOrgConnections = orgAppConnections.filter((connection) =>
orgPermission.can( orgPermission.can(
OrgPermissionAppConnectionActions.Connect, OrgPermissionAppConnectionActions.Connect,
subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id }) subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id })
) )
); );
return availableConnections as Omit<TAppConnection, "credentials">[]; return [...availableOrgConnections, ...availableProjectConnections].sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
) as Omit<TAppConnection, "credentials">[];
};
const findAppConnectionUsageById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
appConnection.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read,
OrgPermissionSubjects.AppConnections
);
if (appConnection.app !== app)
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
const projectUsage = await appConnectionDAL.findAppConnectionUsageById(connectionId);
return projectUsage;
}; };
return { return {
listAppConnectionOptions, listAppConnectionOptions,
listAppConnectionsByOrg, listAppConnections,
findAppConnectionById, findAppConnectionById,
findAppConnectionByName, findAppConnectionByName,
createAppConnection, createAppConnection,
updateAppConnection, updateAppConnection,
deleteAppConnection, deleteAppConnection,
connectAppConnectionById, connectAppConnectionById,
validateAppConnectionUsageById,
listAvailableAppConnectionsForUser, listAvailableAppConnectionsForUser,
findAppConnectionUsageById,
github: githubConnectionService(connectAppConnectionById, gatewayService, gatewayV2Service), github: githubConnectionService(connectAppConnectionById, gatewayService, gatewayV2Service),
githubRadar: githubRadarConnectionService(connectAppConnectionById), githubRadar: githubRadarConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById), gcp: gcpConnectionService(connectAppConnectionById),
@@ -316,13 +316,23 @@ export type TSqlConnectionInput =
export type TCreateAppConnectionDTO = Pick< export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput, TAppConnectionInput,
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId" "credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId" | "projectId"
>; >;
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & { export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app" | "projectId">> & {
connectionId: string; connectionId: string;
}; };
export type TGetAppConnectionByNameDTO = {
connectionName: string;
projectId?: string;
};
export type TValidateAppConnectionUsageByIdDTO = {
connectionId: string;
projectId: string;
};
export type TAppConnectionConfig = export type TAppConnectionConfig =
| TAwsConnectionConfig | TAwsConnectionConfig
| TGitHubConnectionConfig | TGitHubConnectionConfig
@@ -51,7 +51,7 @@ const authorizeAuth0Connection = async ({
}; };
export const getAuth0ConnectionAccessToken = async ( export const getAuth0ConnectionAccessToken = async (
{ id, orgId, credentials }: TAuth0Connection, { id, orgId, credentials, projectId }: TAuth0Connection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">, appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => { ) => {
@@ -72,7 +72,8 @@ export const getAuth0ConnectionAccessToken = async (
const encryptedCredentials = await encryptAppConnectionCredentials({ const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials, credentials: updatedCredentials,
orgId, orgId,
kmsService kmsService,
projectId
}); });
await appConnectionDAL.updateById(id, { encryptedCredentials }); await appConnectionDAL.updateById(id, { encryptedCredentials });
@@ -352,7 +352,8 @@ export const getAzureADCSConnectionCredentials = async (
const credentials = (await decryptAppConnectionCredentials({ const credentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as { })) as {
username: string; username: string;
password: string; password: string;
@@ -57,6 +57,7 @@ export const getAzureConnectionAccessToken = async (
const credentials = (await decryptAppConnectionCredentials({ const credentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
projectId: appConnection.projectId,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureClientSecretsConnectionCredentials; })) as TAzureClientSecretsConnectionCredentials;
@@ -93,6 +94,7 @@ export const getAzureConnectionAccessToken = async (
const encryptedCredentials = await encryptAppConnectionCredentials({ const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials, credentials: updatedCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService kmsService
}); });
@@ -102,6 +104,7 @@ export const getAzureConnectionAccessToken = async (
case AzureClientSecretsConnectionMethod.ClientSecret: case AzureClientSecretsConnectionMethod.ClientSecret:
const accessTokenCredentials = (await decryptAppConnectionCredentials({ const accessTokenCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureClientSecretsConnectionClientSecretCredentials; })) as TAzureClientSecretsConnectionClientSecretCredentials;
@@ -129,6 +132,7 @@ export const getAzureConnectionAccessToken = async (
const encryptedClientCredentials = await encryptAppConnectionCredentials({ const encryptedClientCredentials = await encryptAppConnectionCredentials({
credentials: updatedClientCredentials, credentials: updatedClientCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
projectId: appConnection.projectId,
kmsService kmsService
}); });
@@ -70,7 +70,8 @@ export const getAzureDevopsConnection = async (
const oauthCredentials = (await decryptAppConnectionCredentials({ const oauthCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as TAzureDevOpsConnectionCredentials; })) as TAzureDevOpsConnectionCredentials;
if (!("refreshToken" in oauthCredentials)) { if (!("refreshToken" in oauthCredentials)) {
@@ -100,7 +101,8 @@ export const getAzureDevopsConnection = async (
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({ const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
credentials: updatedOAuthCredentials, credentials: updatedOAuthCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}); });
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
@@ -111,7 +113,8 @@ export const getAzureDevopsConnection = async (
const accessTokenCredentials = (await decryptAppConnectionCredentials({ const accessTokenCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as { accessToken: string }; })) as { accessToken: string };
if (!("accessToken" in accessTokenCredentials)) { if (!("accessToken" in accessTokenCredentials)) {
@@ -124,7 +127,8 @@ export const getAzureDevopsConnection = async (
const clientSecretCredentials = (await decryptAppConnectionCredentials({ const clientSecretCredentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService, kmsService,
encryptedCredentials: appConnection.encryptedCredentials encryptedCredentials: appConnection.encryptedCredentials,
projectId: appConnection.projectId
})) as TAzureDevOpsConnectionClientSecretCredentials; })) as TAzureDevOpsConnectionClientSecretCredentials;
const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials; const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials;
@@ -153,7 +157,8 @@ export const getAzureDevopsConnection = async (
const encryptedClientCredentials = await encryptAppConnectionCredentials({ const encryptedClientCredentials = await encryptAppConnectionCredentials({
credentials: updatedClientCredentials, credentials: updatedClientCredentials,
orgId: appConnection.orgId, orgId: appConnection.orgId,
kmsService kmsService,
projectId: appConnection.projectId
}); });
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials }); await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });

Some files were not shown because too many files have changed in this diff Show More