Merge pull request #2199 from Infisical/secret-engine-v2-bridge

Secret engine v2 bridge
This commit is contained in:
Maidul Islam
2024-07-30 21:41:21 -04:00
committed by GitHub
192 changed files with 13069 additions and 2766 deletions
@@ -0,0 +1,576 @@
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 = {
workspaceId: 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/v3/secrets/raw/${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/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: 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 V2 Architecture - $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/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: 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/v1/folders/${folderId}`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: 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/v3/secrets/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
query: {
secretPath,
environment,
workspaceId: 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/v3/secrets/raw/${secret.key}`,
headers: {
authorization: `Bearer ${authToken}`
},
query: {
secretPath: path,
workspaceId: 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 = {
workspaceId: 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/v3/secrets/raw/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 = {
workspaceId: 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/v3/secrets/raw/${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 = {
workspaceId: 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/v3/secrets/raw/${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/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: 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/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: 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/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: 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 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/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: 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
})
)
)
);
});
}
);
+2 -2
View File
@@ -40,8 +40,8 @@
"type:check": "tsc --noEmit", "type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src", "lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'", "lint": "eslint 'src/**/*.ts'",
"test:e2e": "vitest run -c vitest.e2e.config.ts", "test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts", "generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts",
+70
View File
@@ -204,6 +204,9 @@ import {
TSecretApprovalRequestSecretTags, TSecretApprovalRequestSecretTags,
TSecretApprovalRequestSecretTagsInsert, TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate, TSecretApprovalRequestSecretTagsUpdate,
TSecretApprovalRequestSecretTagsV2,
TSecretApprovalRequestSecretTagsV2Insert,
TSecretApprovalRequestSecretTagsV2Update,
TSecretApprovalRequestsInsert, TSecretApprovalRequestsInsert,
TSecretApprovalRequestsReviewers, TSecretApprovalRequestsReviewers,
TSecretApprovalRequestsReviewersInsert, TSecretApprovalRequestsReviewersInsert,
@@ -211,6 +214,9 @@ import {
TSecretApprovalRequestsSecrets, TSecretApprovalRequestsSecrets,
TSecretApprovalRequestsSecretsInsert, TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsUpdate, TSecretApprovalRequestsSecretsUpdate,
TSecretApprovalRequestsSecretsV2,
TSecretApprovalRequestsSecretsV2Insert,
TSecretApprovalRequestsSecretsV2Update,
TSecretApprovalRequestsUpdate, TSecretApprovalRequestsUpdate,
TSecretBlindIndexes, TSecretBlindIndexes,
TSecretBlindIndexesInsert, TSecretBlindIndexesInsert,
@@ -227,9 +233,15 @@ import {
TSecretReferences, TSecretReferences,
TSecretReferencesInsert, TSecretReferencesInsert,
TSecretReferencesUpdate, TSecretReferencesUpdate,
TSecretReferencesV2,
TSecretReferencesV2Insert,
TSecretReferencesV2Update,
TSecretRotationOutputs, TSecretRotationOutputs,
TSecretRotationOutputsInsert, TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate, TSecretRotationOutputsUpdate,
TSecretRotationOutputV2,
TSecretRotationOutputV2Insert,
TSecretRotationOutputV2Update,
TSecretRotations, TSecretRotations,
TSecretRotationsInsert, TSecretRotationsInsert,
TSecretRotationsUpdate, TSecretRotationsUpdate,
@@ -248,6 +260,9 @@ import {
TSecretSnapshotSecrets, TSecretSnapshotSecrets,
TSecretSnapshotSecretsInsert, TSecretSnapshotSecretsInsert,
TSecretSnapshotSecretsUpdate, TSecretSnapshotSecretsUpdate,
TSecretSnapshotSecretsV2,
TSecretSnapshotSecretsV2Insert,
TSecretSnapshotSecretsV2Update,
TSecretSnapshotsInsert, TSecretSnapshotsInsert,
TSecretSnapshotsUpdate, TSecretSnapshotsUpdate,
TSecretsUpdate, TSecretsUpdate,
@@ -263,6 +278,9 @@ import {
TSecretVersionTagJunction, TSecretVersionTagJunction,
TSecretVersionTagJunctionInsert, TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate, TSecretVersionTagJunctionUpdate,
TSecretVersionV2TagJunction,
TSecretVersionV2TagJunctionInsert,
TSecretVersionV2TagJunctionUpdate,
TServiceTokens, TServiceTokens,
TServiceTokensInsert, TServiceTokensInsert,
TServiceTokensUpdate, TServiceTokensUpdate,
@@ -291,6 +309,17 @@ import {
TWebhooksInsert, TWebhooksInsert,
TWebhooksUpdate TWebhooksUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import {
TSecretV2TagJunction,
TSecretV2TagJunctionInsert,
TSecretV2TagJunctionUpdate
} from "@app/db/schemas/secret-v2-tag-junction";
import {
TSecretVersionsV2,
TSecretVersionsV2Insert,
TSecretVersionsV2Update
} from "@app/db/schemas/secret-versions-v2";
import { TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas/secrets-v2";
declare module "knex" { declare module "knex" {
namespace Knex { namespace Knex {
@@ -645,7 +674,23 @@ declare module "knex/types/tables" {
TSecretScanningGitRisksUpdate TSecretScanningGitRisksUpdate
>; >;
[TableName.TrustedIps]: KnexOriginal.CompositeTableType<TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate>; [TableName.TrustedIps]: KnexOriginal.CompositeTableType<TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate>;
[TableName.SecretV2]: KnexOriginal.CompositeTableType<TSecretsV2, TSecretsV2Insert, TSecretsV2Update>;
[TableName.SecretVersionV2]: KnexOriginal.CompositeTableType<
TSecretVersionsV2,
TSecretVersionsV2Insert,
TSecretVersionsV2Update
>;
[TableName.SecretReferenceV2]: KnexOriginal.CompositeTableType<
TSecretReferencesV2,
TSecretReferencesV2Insert,
TSecretReferencesV2Update
>;
// Junction tables // Junction tables
[TableName.SecretV2JnTag]: KnexOriginal.CompositeTableType<
TSecretV2TagJunction,
TSecretV2TagJunctionInsert,
TSecretV2TagJunctionUpdate
>;
[TableName.JnSecretTag]: KnexOriginal.CompositeTableType< [TableName.JnSecretTag]: KnexOriginal.CompositeTableType<
TSecretTagJunction, TSecretTagJunction,
TSecretTagJunctionInsert, TSecretTagJunctionInsert,
@@ -656,6 +701,31 @@ declare module "knex/types/tables" {
TSecretVersionTagJunctionInsert, TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate TSecretVersionTagJunctionUpdate
>; >;
[TableName.SecretVersionV2Tag]: KnexOriginal.CompositeTableType<
TSecretVersionV2TagJunction,
TSecretVersionV2TagJunctionInsert,
TSecretVersionV2TagJunctionUpdate
>;
[TableName.SnapshotSecretV2]: KnexOriginal.CompositeTableType<
TSecretSnapshotSecretsV2,
TSecretSnapshotSecretsV2Insert,
TSecretSnapshotSecretsV2Update
>;
[TableName.SecretApprovalRequestSecretV2]: KnexOriginal.CompositeTableType<
TSecretApprovalRequestsSecretsV2,
TSecretApprovalRequestsSecretsV2Insert,
TSecretApprovalRequestsSecretsV2Update
>;
[TableName.SecretApprovalRequestSecretTagV2]: KnexOriginal.CompositeTableType<
TSecretApprovalRequestSecretTagsV2,
TSecretApprovalRequestSecretTagsV2Insert,
TSecretApprovalRequestSecretTagsV2Update
>;
[TableName.SecretRotationOutputV2]: KnexOriginal.CompositeTableType<
TSecretRotationOutputV2,
TSecretRotationOutputV2Insert,
TSecretRotationOutputV2Update
>;
// KMS service // KMS service
[TableName.KmsServerRootConfig]: KnexOriginal.CompositeTableType< [TableName.KmsServerRootConfig]: KnexOriginal.CompositeTableType<
TKmsRootConfig, TKmsRootConfig,
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
await knex.schema.alterTable(TableName.Organization, (tb) => {
if (!hasKmsDataKeyCol) {
tb.binary("kmsEncryptedDataKey");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
await knex.schema.alterTable(TableName.Organization, (t) => {
if (hasKmsDataKeyCol) {
t.dropColumn("kmsEncryptedDataKey");
}
});
}
@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
TableName.Project,
"kmsSecretManagerEncryptedDataKey"
);
await knex.schema.alterTable(TableName.Project, (tb) => {
if (!hasKmsSecretManagerEncryptedDataKey) {
tb.binary("kmsSecretManagerEncryptedDataKey");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
TableName.Project,
"kmsSecretManagerEncryptedDataKey"
);
await knex.schema.alterTable(TableName.Project, (t) => {
if (hasKmsSecretManagerEncryptedDataKey) {
t.dropColumn("kmsSecretManagerEncryptedDataKey");
}
});
}
@@ -0,0 +1,404 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Knex } from "knex";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { selectAllTableCols } from "@app/lib/knex/select";
import { SecretKeyEncoding, SecretType, TableName } from "../schemas";
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
import { getSecretManagerDataKey } from "./utils/kms";
const backfillWebhooks = async (knex: Knex) => {
const hasEncryptedSecretKeyWithKms = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
await knex.schema.alterTable(TableName.Webhook, (t) => {
if (!hasEncryptedSecretKeyWithKms) t.binary("encryptedSecretKeyWithKms");
if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl");
if (hasUrl) t.string("url").nullable().alter();
});
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
{};
if (hasUrlCipherText && hasUrlIV && hasUrlTag && hasEncryptedSecretKey && hasIV && hasTag) {
// eslint-disable-next-line
const webhooksToFill = await knex(TableName.Webhook)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`)
.whereNull("encryptedUrl")
// eslint-disable-next-line
// @ts-ignore knex migration fails
.select(selectAllTableCols(TableName.Webhook))
.select("projectId");
const updatedWebhooks = [];
for (const webhook of webhooksToFill) {
if (!kmsEncryptorGroupByProjectId[webhook.projectId]) {
// eslint-disable-next-line
const { encryptor } = await getSecretManagerDataKey(knex, webhook.projectId);
kmsEncryptorGroupByProjectId[webhook.projectId] = encryptor;
}
const kmsEncryptor = kmsEncryptorGroupByProjectId[webhook.projectId];
// @ts-ignore post migration fails
let webhookUrl = webhook.url;
let webhookSecretKey;
// @ts-ignore post migration fails
if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) {
webhookUrl = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: webhook.urlCipherText,
// @ts-ignore post migration fails
iv: webhook.urlIV,
// @ts-ignore post migration fails
tag: webhook.urlTag
});
}
// @ts-ignore post migration fails
if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) {
webhookSecretKey = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: webhook.encryptedSecretKey,
// @ts-ignore post migration fails
iv: webhook.iv,
// @ts-ignore post migration fails
tag: webhook.tag
});
}
const { projectId, ...el } = webhook;
updatedWebhooks.push({
...el,
encryptedSecretKeyWithKms: webhookSecretKey
? kmsEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob
: null,
encryptedUrl: kmsEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob
});
}
if (updatedWebhooks.length) {
// eslint-disable-next-line
await knex(TableName.Webhook).insert(updatedWebhooks).onConflict("id").merge();
}
}
await knex.schema.alterTable(TableName.Webhook, (t) => {
t.binary("encryptedUrl").notNullable().alter();
if (hasUrlIV) t.dropColumn("urlIV");
if (hasUrlCipherText) t.dropColumn("urlCipherText");
if (hasUrlTag) t.dropColumn("urlTag");
if (hasIV) t.dropColumn("iv");
if (hasTag) t.dropColumn("tag");
if (hasEncryptedSecretKey) t.dropColumn("encryptedSecretKey");
if (hasKeyEncoding) t.dropColumn("keyEncoding");
if (hasAlgorithm) t.dropColumn("algorithm");
if (hasUrl) t.dropColumn("url");
});
};
const backfillDynamicSecretConfigs = async (knex: Knex) => {
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (!hasEncryptedConfig) t.binary("encryptedConfig");
});
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
{};
if (hasInputCipherText && hasInputIV && hasInputTag) {
// eslint-disable-next-line
const dynamicSecretConfigs = await knex(TableName.DynamicSecret)
.join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.whereNull("encryptedConfig")
// @ts-ignore post migration fails
.select(selectAllTableCols(TableName.DynamicSecret))
.select("projectId");
const updatedConfigs = [];
for (const dynamicSecretConfig of dynamicSecretConfigs) {
if (!kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]) {
// eslint-disable-next-line
const { encryptor } = await getSecretManagerDataKey(knex, dynamicSecretConfig.projectId);
kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId] = encryptor;
}
const kmsEncryptor = kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId];
const inputConfig = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: dynamicSecretConfig.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: dynamicSecretConfig.inputCiphertext as string,
// @ts-ignore post migration fails
iv: dynamicSecretConfig.inputIV as string,
// @ts-ignore post migration fails
tag: dynamicSecretConfig.inputTag as string
});
const { projectId, ...el } = dynamicSecretConfig;
updatedConfigs.push({
...el,
encryptedConfig: kmsEncryptor({ plainText: Buffer.from(inputConfig) }).cipherTextBlob
});
}
if (updatedConfigs.length) {
// eslint-disable-next-line
await knex(TableName.DynamicSecret).insert(updatedConfigs).onConflict("id").merge();
}
}
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
t.binary("encryptedConfig").notNullable().alter();
if (hasInputTag) t.dropColumn("inputTag");
if (hasInputIV) t.dropColumn("inputIV");
if (hasInputCipherText) t.dropColumn("inputCiphertext");
if (hasKeyEncoding) t.dropColumn("keyEncoding");
if (hasAlgorithm) t.dropColumn("algorithm");
});
};
export async function up(knex: Knex): Promise<void> {
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
if (!doesSecretV2TableExist) {
await knex.schema.createTable(TableName.SecretV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.integer("version").defaultTo(1).notNullable();
t.string("type").notNullable().defaultTo(SecretType.Shared);
t.string("key", 500).notNullable();
t.binary("encryptedValue");
t.binary("encryptedComment");
t.string("reminderNote");
t.integer("reminderRepeatDays");
t.boolean("skipMultilineEncoding").defaultTo(false);
t.jsonb("metadata");
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("folderId").notNullable();
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
t.timestamps(true, true, true);
t.index(["folderId", "userId"]);
});
}
await createOnUpdateTrigger(knex, TableName.SecretV2);
// many to many relation between tags
await createJunctionTable(knex, TableName.SecretV2JnTag, TableName.SecretV2, TableName.SecretTag);
const doesSecretV2VersionTableExist = await knex.schema.hasTable(TableName.SecretVersionV2);
if (!doesSecretV2VersionTableExist) {
await knex.schema.createTable(TableName.SecretVersionV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.integer("version").defaultTo(1).notNullable();
t.string("type").notNullable().defaultTo(SecretType.Shared);
t.string("key", 500).notNullable();
t.binary("encryptedValue");
t.binary("encryptedComment");
t.string("reminderNote");
t.integer("reminderRepeatDays");
t.boolean("skipMultilineEncoding").defaultTo(false);
t.jsonb("metadata");
// to avoid orphan rows
t.uuid("envId");
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
t.uuid("secretId").notNullable();
t.uuid("folderId").notNullable();
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.SecretVersionV2);
if (!(await knex.schema.hasTable(TableName.SecretReferenceV2))) {
await knex.schema.createTable(TableName.SecretReferenceV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("environment").notNullable();
t.string("secretPath").notNullable();
t.string("secretKey", 500).notNullable();
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
});
}
await createJunctionTable(knex, TableName.SecretVersionV2Tag, TableName.SecretVersionV2, TableName.SecretTag);
if (!(await knex.schema.hasTable(TableName.SecretApprovalRequestSecretV2))) {
await knex.schema.createTable(TableName.SecretApprovalRequestSecretV2, (t) => {
// everything related to secret
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.integer("version").defaultTo(1);
t.string("key", 500).notNullable();
t.binary("encryptedValue");
t.binary("encryptedComment");
t.string("reminderNote");
t.integer("reminderRepeatDays");
t.boolean("skipMultilineEncoding").defaultTo(false);
t.jsonb("metadata");
t.timestamps(true, true, true);
// commit details
t.uuid("requestId").notNullable();
t.foreign("requestId").references("id").inTable(TableName.SecretApprovalRequest).onDelete("CASCADE");
t.string("op").notNullable();
t.uuid("secretId");
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("SET NULL");
t.uuid("secretVersion");
t.foreign("secretVersion").references("id").inTable(TableName.SecretVersionV2).onDelete("SET NULL");
});
}
if (!(await knex.schema.hasTable(TableName.SecretApprovalRequestSecretTagV2))) {
await knex.schema.createTable(TableName.SecretApprovalRequestSecretTagV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.SecretApprovalRequestSecretV2).onDelete("CASCADE");
t.uuid("tagId").notNullable();
t.foreign("tagId").references("id").inTable(TableName.SecretTag).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
if (!(await knex.schema.hasTable(TableName.SnapshotSecretV2))) {
await knex.schema.createTable(TableName.SnapshotSecretV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("envId").index().notNullable();
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
// not a relation kept like that to keep it when rolled back
t.uuid("secretVersionId").index().notNullable();
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
t.uuid("snapshotId").index().notNullable();
t.foreign("snapshotId").references("id").inTable(TableName.Snapshot).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
const hasEncryptedAccess = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccess");
const hasEncryptedAccessId = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccessId");
const hasEncryptedRefresh = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedRefresh");
const hasEncryptedAwsIamAssumRole = await knex.schema.hasColumn(
TableName.IntegrationAuth,
"encryptedAwsAssumeIamRoleArn"
);
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
if (!hasEncryptedAccess) t.binary("encryptedAccess");
if (!hasEncryptedAccessId) t.binary("encryptedAccessId");
if (!hasEncryptedRefresh) t.binary("encryptedRefresh");
if (!hasEncryptedAwsIamAssumRole) t.binary("encryptedAwsAssumeIamRoleArn");
});
}
if (!(await knex.schema.hasTable(TableName.SecretRotationOutputV2))) {
await knex.schema.createTable(TableName.SecretRotationOutputV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("key").notNullable();
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
t.uuid("rotationId").notNullable();
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
});
}
if (await knex.schema.hasTable(TableName.Webhook)) {
await backfillWebhooks(knex);
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
await backfillDynamicSecretConfigs(knex);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SnapshotSecretV2);
await knex.schema.dropTableIfExists(TableName.SecretApprovalRequestSecretTagV2);
await knex.schema.dropTableIfExists(TableName.SecretApprovalRequestSecretV2);
await knex.schema.dropTableIfExists(TableName.SecretV2JnTag);
await knex.schema.dropTableIfExists(TableName.SecretReferenceV2);
await knex.schema.dropTableIfExists(TableName.SecretRotationOutputV2);
await dropOnUpdateTrigger(knex, TableName.SecretVersionV2);
await knex.schema.dropTableIfExists(TableName.SecretVersionV2Tag);
await knex.schema.dropTableIfExists(TableName.SecretVersionV2);
await dropOnUpdateTrigger(knex, TableName.SecretV2);
await knex.schema.dropTableIfExists(TableName.SecretV2);
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
const hasEncryptedAccess = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccess");
const hasEncryptedAccessId = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccessId");
const hasEncryptedRefresh = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedRefresh");
const hasEncryptedAwsIamAssumRole = await knex.schema.hasColumn(
TableName.IntegrationAuth,
"encryptedAwsAssumeIamRoleArn"
);
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
if (hasEncryptedAccess) t.dropColumn("encryptedAccess");
if (hasEncryptedAccessId) t.dropColumn("encryptedAccessId");
if (hasEncryptedRefresh) t.dropColumn("encryptedRefresh");
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
});
}
if (await knex.schema.hasTable(TableName.Webhook)) {
const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
await knex.schema.alterTable(TableName.Webhook, (t) => {
if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms");
if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl");
if (!hasUrl) t.string("url");
if (!hasEncryptedSecretKey) t.string("encryptedSecretKey");
if (!hasIV) t.string("iv");
if (!hasTag) t.string("tag");
if (!hasAlgorithm) t.string("algorithm");
if (!hasKeyEncoding) t.string("keyEncoding");
if (!hasUrlCipherText) t.string("urlCipherText");
if (!hasUrlIV) t.string("urlIV");
if (!hasUrlTag) t.string("urlTag");
});
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (hasEncryptedConfig) t.dropColumn("encryptedConfig");
if (!hasInputIV) t.string("inputIV");
if (!hasInputCipherText) t.text("inputCiphertext");
if (!hasInputTag) t.string("inputTag");
if (!hasAlgorithm) t.string("algorithm");
if (!hasKeyEncoding) t.string("keyEncoding");
});
}
}
+105
View File
@@ -0,0 +1,105 @@
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { alphaNumericNanoId } from "@app/lib/nanoid";
const getInstanceRootKey = async (knex: Knex) => {
const encryptionKey = process.env.ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = !process.env.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("ENCRYPTION_KEY variable needed for migration");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const kmsRootConfig = await knex(TableName.KmsServerRootConfig).where({ id: KMS_ROOT_CONFIG_UUID }).first();
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
if (kmsRootConfig) {
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
// set the flag so that other instancen nodes can start
return decryptedRootKey;
}
const newRootKey = randomSecureBytes(32);
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
await knex(TableName.KmsServerRootConfig).insert({
encryptedRootKey,
// eslint-disable-next-line
// @ts-ignore id is kept as fixed for idempotence and to avoid race condition
id: KMS_ROOT_CONFIG_UUID
});
return encryptedRootKey;
};
export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => {
const KMS_VERSION = "v01";
const KMS_VERSION_BLOB_LENGTH = 3;
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const project = await knex(TableName.Project).where({ id: projectId }).first();
if (!project) throw new Error("Missing project id");
const ROOT_ENCRYPTION_KEY = await getInstanceRootKey(knex);
let secretManagerKmsKey;
const projectSecretManagerKmsId = project?.kmsSecretManagerKeyId;
if (projectSecretManagerKmsId) {
const kmsDoc = await knex(TableName.KmsKey)
.leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
.where({ [`${TableName.KmsKey}.id` as "id"]: projectSecretManagerKmsId })
.first();
if (!kmsDoc) throw new Error("missing kms");
secretManagerKmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
} else {
const [kmsDoc] = await knex(TableName.KmsKey)
.insert({
slug: slugify(alphaNumericNanoId(8).toLowerCase()),
orgId: project.orgId,
isReserved: false
})
.returning("*");
secretManagerKmsKey = randomSecureBytes(32);
const encryptedKeyMaterial = cipher.encrypt(secretManagerKmsKey, ROOT_ENCRYPTION_KEY);
await knex(TableName.InternalKms).insert({
version: 1,
encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
kmsKeyId: kmsDoc.id
});
}
const encryptedSecretManagerDataKey = project?.kmsSecretManagerEncryptedDataKey;
let dataKey: Buffer;
if (!encryptedSecretManagerDataKey) {
dataKey = randomSecureBytes();
// the below versioning we do it automatically in kms service
const unversionedDataKey = cipher.encrypt(dataKey, secretManagerKmsKey);
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
await knex(TableName.Project)
.where({ id: projectId })
.update({
kmsSecretManagerEncryptedDataKey: Buffer.concat([unversionedDataKey, versionBlob])
});
} else {
const cipherTextBlob = encryptedSecretManagerDataKey.subarray(0, -KMS_VERSION_BLOB_LENGTH);
dataKey = cipher.decrypt(cipherTextBlob, secretManagerKmsKey);
}
return {
encryptor: ({ plainText }: { plainText: Buffer }) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
},
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: { cipherTextBlob: Buffer }) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
return decryptedBlob;
}
};
};
+4 -6
View File
@@ -5,6 +5,8 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({ export const DynamicSecretsSchema = z.object({
@@ -14,16 +16,12 @@ export const DynamicSecretsSchema = z.object({
type: z.string(), type: z.string(),
defaultTTL: z.string(), defaultTTL: z.string(),
maxTTL: z.string().nullable().optional(), maxTTL: z.string().nullable().optional(),
inputIV: z.string(),
inputCiphertext: z.string(),
inputTag: z.string(),
algorithm: z.string().default("aes-256-gcm"),
keyEncoding: z.string().default("utf8"),
folderId: z.string().uuid(), folderId: z.string().uuid(),
status: z.string().nullable().optional(), status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
encryptedConfig: zodBuffer
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
+9
View File
@@ -66,26 +66,35 @@ export * from "./scim-tokens";
export * from "./secret-approval-policies"; export * from "./secret-approval-policies";
export * from "./secret-approval-policies-approvers"; export * from "./secret-approval-policies-approvers";
export * from "./secret-approval-request-secret-tags"; export * from "./secret-approval-request-secret-tags";
export * from "./secret-approval-request-secret-tags-v2";
export * from "./secret-approval-requests"; export * from "./secret-approval-requests";
export * from "./secret-approval-requests-reviewers"; export * from "./secret-approval-requests-reviewers";
export * from "./secret-approval-requests-secrets"; export * from "./secret-approval-requests-secrets";
export * from "./secret-approval-requests-secrets-v2";
export * from "./secret-blind-indexes"; export * from "./secret-blind-indexes";
export * from "./secret-folder-versions"; export * from "./secret-folder-versions";
export * from "./secret-folders"; export * from "./secret-folders";
export * from "./secret-imports"; export * from "./secret-imports";
export * from "./secret-references"; export * from "./secret-references";
export * from "./secret-references-v2";
export * from "./secret-rotation-output-v2";
export * from "./secret-rotation-outputs"; export * from "./secret-rotation-outputs";
export * from "./secret-rotations"; export * from "./secret-rotations";
export * from "./secret-scanning-git-risks"; export * from "./secret-scanning-git-risks";
export * from "./secret-sharing"; export * from "./secret-sharing";
export * from "./secret-snapshot-folders"; export * from "./secret-snapshot-folders";
export * from "./secret-snapshot-secrets"; export * from "./secret-snapshot-secrets";
export * from "./secret-snapshot-secrets-v2";
export * from "./secret-snapshots"; export * from "./secret-snapshots";
export * from "./secret-tag-junction"; export * from "./secret-tag-junction";
export * from "./secret-tags"; export * from "./secret-tags";
export * from "./secret-v2-tag-junction";
export * from "./secret-version-tag-junction"; export * from "./secret-version-tag-junction";
export * from "./secret-version-v2-tag-junction";
export * from "./secret-versions"; export * from "./secret-versions";
export * from "./secret-versions-v2";
export * from "./secrets"; export * from "./secrets";
export * from "./secrets-v2";
export * from "./service-tokens"; export * from "./service-tokens";
export * from "./super-admin"; export * from "./super-admin";
export * from "./trusted-ips"; export * from "./trusted-ips";
+7 -1
View File
@@ -5,6 +5,8 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const IntegrationAuthsSchema = z.object({ export const IntegrationAuthsSchema = z.object({
@@ -32,7 +34,11 @@ export const IntegrationAuthsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
awsAssumeIamRoleArnCipherText: z.string().nullable().optional(), awsAssumeIamRoleArnCipherText: z.string().nullable().optional(),
awsAssumeIamRoleArnIV: z.string().nullable().optional(), awsAssumeIamRoleArnIV: z.string().nullable().optional(),
awsAssumeIamRoleArnTag: z.string().nullable().optional() awsAssumeIamRoleArnTag: z.string().nullable().optional(),
encryptedAccess: zodBuffer.nullable().optional(),
encryptedAccessId: zodBuffer.nullable().optional(),
encryptedRefresh: zodBuffer.nullable().optional(),
encryptedAwsAssumeIamRoleArn: zodBuffer.nullable().optional()
}); });
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>; export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;
+11 -1
View File
@@ -90,9 +90,18 @@ export enum TableName {
TrustedIps = "trusted_ips", TrustedIps = "trusted_ips",
DynamicSecret = "dynamic_secrets", DynamicSecret = "dynamic_secrets",
DynamicSecretLease = "dynamic_secret_leases", DynamicSecretLease = "dynamic_secret_leases",
SecretV2 = "secrets_v2",
SecretReferenceV2 = "secret_references_v2",
SecretVersionV2 = "secret_versions_v2",
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
// junction tables with tags // junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction", JnSecretTag = "secret_tag_junction",
SecretVersionTag = "secret_version_tag_junction", SecretVersionTag = "secret_version_tag_junction",
SecretVersionV2Tag = "secret_version_v2_tag_junction",
SecretRotationOutputV2 = "secret_rotation_output_v2",
// KMS Service // KMS Service
KmsServerRootConfig = "kms_root_config", KmsServerRootConfig = "kms_root_config",
KmsKey = "kms_keys", KmsKey = "kms_keys",
@@ -157,7 +166,8 @@ export enum SecretType {
export enum ProjectVersion { export enum ProjectVersion {
V1 = 1, V1 = 1,
V2 = 2 V2 = 2,
V3 = 3
} }
export enum ProjectUpgradeStatus { export enum ProjectUpgradeStatus {
+4 -1
View File
@@ -5,6 +5,8 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const OrganizationsSchema = z.object({ export const OrganizationsSchema = z.object({
@@ -16,7 +18,8 @@ export const OrganizationsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
authEnforced: z.boolean().default(false).nullable().optional(), authEnforced: z.boolean().default(false).nullable().optional(),
scimEnabled: z.boolean().default(false).nullable().optional(), scimEnabled: z.boolean().default(false).nullable().optional(),
kmsDefaultKeyId: z.string().uuid().nullable().optional() kmsDefaultKeyId: z.string().uuid().nullable().optional(),
kmsEncryptedDataKey: zodBuffer.nullable().optional()
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;
+4 -1
View File
@@ -5,6 +5,8 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const ProjectsSchema = z.object({ export const ProjectsSchema = z.object({
@@ -20,7 +22,8 @@ export const ProjectsSchema = z.object({
pitVersionLimit: z.number().default(10), pitVersionLimit: z.number().default(10),
kmsCertificateKeyId: z.string().uuid().nullable().optional(), kmsCertificateKeyId: z.string().uuid().nullable().optional(),
auditLogsRetentionDays: z.number().nullable().optional(), auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional() kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalRequestSecretTagsV2Schema = z.object({
id: z.string().uuid(),
secretId: z.string().uuid(),
tagId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretApprovalRequestSecretTagsV2 = z.infer<typeof SecretApprovalRequestSecretTagsV2Schema>;
export type TSecretApprovalRequestSecretTagsV2Insert = Omit<
z.input<typeof SecretApprovalRequestSecretTagsV2Schema>,
TImmutableDBKeys
>;
export type TSecretApprovalRequestSecretTagsV2Update = Partial<
Omit<z.input<typeof SecretApprovalRequestSecretTagsV2Schema>, TImmutableDBKeys>
>;
@@ -0,0 +1,37 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretApprovalRequestsSecretsV2Schema = z.object({
id: z.string().uuid(),
version: z.number().default(1).nullable().optional(),
key: z.string(),
encryptedValue: zodBuffer.nullable().optional(),
encryptedComment: zodBuffer.nullable().optional(),
reminderNote: z.string().nullable().optional(),
reminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
requestId: z.string().uuid(),
op: z.string(),
secretId: z.string().uuid().nullable().optional(),
secretVersion: z.string().uuid().nullable().optional()
});
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;
export type TSecretApprovalRequestsSecretsV2Insert = Omit<
z.input<typeof SecretApprovalRequestsSecretsV2Schema>,
TImmutableDBKeys
>;
export type TSecretApprovalRequestsSecretsV2Update = Partial<
Omit<z.input<typeof SecretApprovalRequestsSecretsV2Schema>, TImmutableDBKeys>
>;
@@ -0,0 +1,20 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretReferencesV2Schema = z.object({
id: z.string().uuid(),
environment: z.string(),
secretPath: z.string(),
secretKey: z.string(),
secretId: z.string().uuid()
});
export type TSecretReferencesV2 = z.infer<typeof SecretReferencesV2Schema>;
export type TSecretReferencesV2Insert = Omit<z.input<typeof SecretReferencesV2Schema>, TImmutableDBKeys>;
export type TSecretReferencesV2Update = Partial<Omit<z.input<typeof SecretReferencesV2Schema>, TImmutableDBKeys>>;
@@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretRotationOutputV2Schema = z.object({
id: z.string().uuid(),
key: z.string(),
secretId: z.string().uuid(),
rotationId: z.string().uuid()
});
export type TSecretRotationOutputV2 = z.infer<typeof SecretRotationOutputV2Schema>;
export type TSecretRotationOutputV2Insert = Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>;
export type TSecretRotationOutputV2Update = Partial<
Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>
>;
@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretSnapshotSecretsV2Schema = z.object({
id: z.string().uuid(),
envId: z.string().uuid(),
secretVersionId: z.string().uuid(),
snapshotId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretSnapshotSecretsV2 = z.infer<typeof SecretSnapshotSecretsV2Schema>;
export type TSecretSnapshotSecretsV2Insert = Omit<z.input<typeof SecretSnapshotSecretsV2Schema>, TImmutableDBKeys>;
export type TSecretSnapshotSecretsV2Update = Partial<
Omit<z.input<typeof SecretSnapshotSecretsV2Schema>, TImmutableDBKeys>
>;
@@ -0,0 +1,18 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretV2TagJunctionSchema = z.object({
id: z.string().uuid(),
secrets_v2Id: z.string().uuid(),
secret_tagsId: z.string().uuid()
});
export type TSecretV2TagJunction = z.infer<typeof SecretV2TagJunctionSchema>;
export type TSecretV2TagJunctionInsert = Omit<z.input<typeof SecretV2TagJunctionSchema>, TImmutableDBKeys>;
export type TSecretV2TagJunctionUpdate = Partial<Omit<z.input<typeof SecretV2TagJunctionSchema>, TImmutableDBKeys>>;
@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretVersionV2TagJunctionSchema = z.object({
id: z.string().uuid(),
secret_versions_v2Id: z.string().uuid(),
secret_tagsId: z.string().uuid()
});
export type TSecretVersionV2TagJunction = z.infer<typeof SecretVersionV2TagJunctionSchema>;
export type TSecretVersionV2TagJunctionInsert = Omit<
z.input<typeof SecretVersionV2TagJunctionSchema>,
TImmutableDBKeys
>;
export type TSecretVersionV2TagJunctionUpdate = Partial<
Omit<z.input<typeof SecretVersionV2TagJunctionSchema>, TImmutableDBKeys>
>;
@@ -0,0 +1,33 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretVersionsV2Schema = z.object({
id: z.string().uuid(),
version: z.number().default(1),
type: z.string().default("shared"),
key: z.string(),
encryptedValue: zodBuffer.nullable().optional(),
encryptedComment: zodBuffer.nullable().optional(),
reminderNote: z.string().nullable().optional(),
reminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
envId: z.string().uuid().nullable().optional(),
secretId: z.string().uuid(),
folderId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
export type TSecretVersionsV2Insert = Omit<z.input<typeof SecretVersionsV2Schema>, TImmutableDBKeys>;
export type TSecretVersionsV2Update = Partial<Omit<z.input<typeof SecretVersionsV2Schema>, TImmutableDBKeys>>;
+31
View File
@@ -0,0 +1,31 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretsV2Schema = z.object({
id: z.string().uuid(),
version: z.number().default(1),
type: z.string().default("shared"),
key: z.string(),
encryptedValue: zodBuffer.nullable().optional(),
encryptedComment: zodBuffer.nullable().optional(),
reminderNote: z.string().nullable().optional(),
reminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
userId: z.string().uuid().nullable().optional(),
folderId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretsV2 = z.infer<typeof SecretsV2Schema>;
export type TSecretsV2Insert = Omit<z.input<typeof SecretsV2Schema>, TImmutableDBKeys>;
export type TSecretsV2Update = Partial<Omit<z.input<typeof SecretsV2Schema>, TImmutableDBKeys>>;
+5 -10
View File
@@ -5,27 +5,22 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({ export const WebhooksSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
secretPath: z.string().default("/"), secretPath: z.string().default("/"),
url: z.string(),
lastStatus: z.string().nullable().optional(), lastStatus: z.string().nullable().optional(),
lastRunErrorMessage: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(),
isDisabled: z.boolean().default(false), isDisabled: z.boolean().default(false),
encryptedSecretKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
algorithm: z.string().nullable().optional(),
keyEncoding: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
envId: z.string().uuid(), envId: z.string().uuid(),
urlCipherText: z.string().nullable().optional(), type: z.string().default("general").nullable().optional(),
urlIV: z.string().nullable().optional(), encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
urlTag: z.string().nullable().optional(), encryptedUrl: zodBuffer
type: z.string().default("general").nullable().optional()
}); });
export type TWebhooks = z.infer<typeof WebhooksSchema>; export type TWebhooks = z.infer<typeof WebhooksSchema>;
+5
View File
@@ -33,6 +33,11 @@ export const seedData1 = {
name: "first project", name: "first project",
slug: "first-project" slug: "first-project"
}, },
projectV3: {
id: "77fa7aed-9288-401e-a4c9-3a9430be62a4",
name: "first project v2",
slug: "first-project-v2"
},
environment: { environment: {
name: "Development", name: "Development",
slug: "dev" slug: "dev"
+50
View File
@@ -0,0 +1,50 @@
import { Knex } from "knex";
import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
{ name: "Staging", slug: "staging" },
{ name: "Production", slug: "prod" }
];
export async function seed(knex: Knex): Promise<void> {
const [projectV2] = await knex(TableName.Project)
.insert({
name: seedData1.projectV3.name,
orgId: seedData1.organization.id,
slug: seedData1.projectV3.slug,
version: ProjectVersion.V3,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.projectV3.id
})
.returning("*");
const projectMembershipV3 = await knex(TableName.ProjectMembership)
.insert({
projectId: projectV2.id,
userId: seedData1.id
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembershipV3[0].id
});
// create default environments and default folders
const projectV3Envs = await knex(TableName.Environment)
.insert(
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
name,
slug,
projectId: seedData1.projectV3.id,
position: index + 1
}))
)
.returning("*");
await knex(TableName.SecretFolder).insert(
projectV3Envs.map(({ id }) => ({ name: "root", envId: id, parentId: null }))
);
}
@@ -86,4 +86,15 @@ export async function seed(knex: Knex): Promise<void> {
role: ProjectMembershipRole.Admin, role: ProjectMembershipRole.Admin,
projectMembershipId: identityProjectMembership[0].id projectMembershipId: identityProjectMembership[0].id
}); });
const identityProjectMembershipV3 = await knex(TableName.IdentityProjectMembership)
.insert({
identityId: seedData1.machineIdentity.id,
projectId: seedData1.projectV3.id
})
.returning("*");
await knex(TableName.IdentityProjectMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: identityProjectMembershipV3[0].id
});
} }
+102 -3
View File
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { import {
ExternalKmsAwsSchema, ExternalKmsAwsSchema,
ExternalKmsInputSchema, ExternalKmsInputSchema,
@@ -19,6 +20,23 @@ const sanitizedExternalSchema = KmsKeysSchema.extend({
}) })
}); });
const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
id: true,
description: true,
isDisabled: true,
createdAt: true,
updatedAt: true,
slug: true
})
.extend({
externalKms: ExternalKmsSchema.pick({
provider: true,
status: true,
statusDetails: true
})
})
.array();
const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({ const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
external: ExternalKmsSchema.pick({ external: ExternalKmsSchema.pick({
id: true, id: true,
@@ -39,8 +57,8 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
slug: z.string().min(1).trim().toLowerCase().optional(), slug: z.string().min(1).trim().toLowerCase(),
description: z.string().min(1).trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsInputSchema provider: ExternalKmsInputSchema
}), }),
response: { response: {
@@ -60,6 +78,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
provider: req.body.provider, provider: req.body.provider,
description: req.body.description description: req.body.description
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_KMS,
metadata: {
kmsId: externalKms.id,
provider: req.body.provider.type,
slug: req.body.slug,
description: req.body.description
}
}
});
return { externalKms }; return { externalKms };
} }
}); });
@@ -76,7 +109,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
}), }),
body: z.object({ body: z.object({
slug: z.string().min(1).trim().toLowerCase().optional(), slug: z.string().min(1).trim().toLowerCase().optional(),
description: z.string().min(1).trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsInputUpdateSchema provider: ExternalKmsInputUpdateSchema
}), }),
response: { response: {
@@ -97,6 +130,21 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
description: req.body.description, description: req.body.description,
id: req.params.id id: req.params.id
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_KMS,
metadata: {
kmsId: externalKms.id,
provider: req.body.provider.type,
slug: req.body.slug,
description: req.body.description
}
}
});
return { externalKms }; return { externalKms };
} }
}); });
@@ -126,6 +174,19 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.id id: req.params.id
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_KMS,
metadata: {
kmsId: externalKms.id,
slug: externalKms.slug
}
}
});
return { externalKms }; return { externalKms };
} }
}); });
@@ -155,10 +216,48 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.id id: req.params.id
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_KMS,
metadata: {
kmsId: externalKms.id,
slug: externalKms.slug
}
}
});
return { externalKms }; return { externalKms };
} }
}); });
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
externalKmsList: sanitizedExternalSchemaForGetAll
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const externalKmsList = await server.services.externalKms.list({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { externalKmsList };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/slug/:slug", url: "/slug/:slug",
+5
View File
@@ -4,6 +4,7 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router"; import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGroupRouter } from "./group-router"; import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router"; import { registerLdapRouter } from "./ldap-router";
@@ -87,4 +88,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
}, },
{ prefix: "/additional-privilege" } { prefix: "/additional-privilege" }
); );
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
}; };
+210 -1
View File
@@ -4,9 +4,10 @@ import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
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 { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit } 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";
import { KmsType } from "@app/services/kms/kms-types";
export const registerProjectRouter = async (server: FastifyZodProvider) => { export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -171,4 +172,212 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => ({ actors: [] }) handler: async () => ({ actors: [] })
}); });
server.route({
method: "GET",
url: "/:workspaceId/kms",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const kmsKey = await server.services.project.getProjectKmsKeys({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return kmsKey;
}
});
server.route({
method: "PATCH",
url: "/:workspaceId/kms",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
kms: z.discriminatedUnion("type", [
z.object({ type: z.literal(KmsType.Internal) }),
z.object({ type: z.literal(KmsType.External), kmsId: z.string() })
])
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
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_KMS,
metadata: {
secretManagerKmsKey: {
id: secretManagerKmsKey.id,
slug: secretManagerKmsKey.slug
}
}
}
});
return {
secretManagerKmsKey
};
}
});
server.route({
method: "GET",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
secretManager: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.getProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_KMS_BACKUP,
metadata: {}
}
});
return backup;
}
});
server.route({
method: "POST",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
backup: z.string().min(1)
}),
response: {
200: z.object({
secretManagerKmsKey: z.object({
id: z.string(),
slug: z.string(),
isExternal: z.boolean()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.loadProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
backup: req.body.backup
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.LOAD_PROJECT_KMS_BACKUP,
metadata: {}
}
});
return backup;
}
});
server.route({
method: "POST",
url: "/:workspaceId/migrate-v3",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const migration = await server.services.secret.startSecretV2Migration({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
return migration;
}
});
}; };
@@ -3,16 +3,14 @@ import { z } from "zod";
import { import {
SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsReviewersSchema,
SecretApprovalRequestsSchema, SecretApprovalRequestsSchema,
SecretApprovalRequestsSecretsSchema,
SecretsSchema,
SecretTagsSchema, SecretTagsSchema,
SecretVersionsSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } 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 { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
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 { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string() }).merge( const approvalRequestUser = z.object({ userId: z.string() }).merge(
@@ -261,46 +259,32 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
committerUser: approvalRequestUser, committerUser: approvalRequestUser,
reviewers: approvalRequestUser.extend({ status: z.string() }).array(), reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
secretPath: z.string(), secretPath: z.string(),
commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true }) commits: secretRawSchema
.merge( .omit({ _id: true, environment: true, workspace: true, type: true, version: true })
z.object({ .extend({
tags: tagSchema, op: z.string(),
secret: SecretsSchema.pick({ tags: tagSchema,
id: true, secret: z
version: true, .object({
secretKeyIV: true, id: z.string(),
secretKeyTag: true, version: z.number(),
secretKeyCiphertext: true, secretKey: z.string(),
secretValueIV: true, secretValue: z.string().optional(),
secretValueTag: true, secretComment: z.string().optional()
secretValueCiphertext: true,
secretCommentIV: true,
secretCommentTag: true,
secretCommentCiphertext: true
}) })
.optional() .optional()
.nullable(), .nullable(),
secretVersion: SecretVersionsSchema.pick({ secretVersion: z
id: true, .object({
version: true, id: z.string(),
secretKeyIV: true, version: z.number(),
secretKeyTag: true, secretKey: z.string(),
secretKeyCiphertext: true, secretValue: z.string().optional(),
secretValueIV: true, secretComment: z.string().optional(),
secretValueTag: true, tags: tagSchema
secretValueCiphertext: true,
secretCommentIV: true,
secretCommentTag: true,
secretCommentCiphertext: true
}) })
.merge( .optional()
z.object({ })
tags: tagSchema
})
)
.optional()
})
)
.array() .array()
}) })
) )
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas"; import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
import { 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";
@@ -112,18 +112,10 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
outputs: z outputs: z
.object({ .object({
key: z.string(), key: z.string(),
secret: SecretsSchema.pick({ secret: z.object({
id: true, secretKey: z.string(),
version: true, id: z.string(),
secretKeyIV: true, version: z.number()
secretKeyTag: true,
secretKeyCiphertext: true,
secretValueIV: true,
secretValueTag: true,
secretValueCiphertext: true,
secretCommentIV: true,
secretCommentTag: true,
secretCommentCiphertext: true
}) })
}) })
.array() .array()
@@ -1,8 +1,8 @@
import { z } from "zod"; import { z } from "zod";
import { SecretVersionsSchema } from "@app/db/schemas";
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 { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerSecretVersionRouter = async (server: FastifyZodProvider) => { export const registerSecretVersionRouter = async (server: FastifyZodProvider) => {
@@ -22,7 +22,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 200: z.object({
secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }).array() secretVersions: secretRawSchema.array()
}) })
} }
}, },
+13 -12
View File
@@ -1,9 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas"; import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
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 { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => { export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@@ -27,17 +28,17 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
slug: z.string(), slug: z.string(),
name: z.string() name: z.string()
}), }),
secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }) secretVersions: secretRawSchema
.merge( .omit({ _id: true, environment: true, workspace: true, type: true })
z.object({ .extend({
tags: SecretTagsSchema.pick({ secretId: z.string(),
id: true, tags: SecretTagsSchema.pick({
slug: true, id: true,
name: true, slug: true,
color: true name: true,
}).array() color: true
}) }).array()
) })
.array(), .array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
createdAt: z.date(), createdAt: z.date(),
@@ -139,7 +139,14 @@ export enum EventType {
GET_CERT = "get-cert", GET_CERT = "get-cert",
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert", REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body" GET_CERT_BODY = "get-cert-body",
CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms",
GET_KMS = "get-kms",
UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -1172,6 +1179,62 @@ interface GetCertBody {
}; };
} }
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
kmsId: string;
provider: string;
slug: string;
description?: string;
};
}
interface DeleteKmsEvent {
type: EventType.DELETE_KMS;
metadata: {
kmsId: string;
slug: string;
};
}
interface UpdateKmsEvent {
type: EventType.UPDATE_KMS;
metadata: {
kmsId: string;
provider: string;
slug?: string;
description?: string;
};
}
interface GetKmsEvent {
type: EventType.GET_KMS;
metadata: {
kmsId: string;
slug: string;
};
}
interface UpdateProjectKmsEvent {
type: EventType.UPDATE_PROJECT_KMS;
metadata: {
secretManagerKmsKey: {
id: string;
slug: string;
};
};
}
interface GetProjectKmsBackupEvent {
type: EventType.GET_PROJECT_KMS_BACKUP;
metadata: Record<string, string>; // no metadata yet
}
interface LoadProjectKmsBackupEvent {
type: EventType.LOAD_PROJECT_KMS_BACKUP;
metadata: Record<string, string>; // no metadata yet
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -1273,4 +1336,11 @@ export type Event =
| GetCert | GetCert
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
| GetCertBody; | GetCertBody
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent
| GetKmsEvent
| UpdateProjectKmsEvent
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent;
@@ -72,7 +72,7 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId kmsId: keyId
}); });
const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl); const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64"); const base64crl = crl.toString("base64");
@@ -40,14 +40,10 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"), db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"), db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"), db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"),
db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"),
db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"),
db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"),
db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"),
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
db.ref("encryptedConfig").withSchema(TableName.DynamicSecret).as("dynEncryptedConfig"),
db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"), db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt") db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
); );
@@ -62,16 +58,12 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
type: doc.dynType, type: doc.dynType,
defaultTTL: doc.dynDefaultTTL, defaultTTL: doc.dynDefaultTTL,
maxTTL: doc.dynMaxTTL, maxTTL: doc.dynMaxTTL,
inputIV: doc.dynInputIV,
inputTag: doc.dynInputTag,
inputCiphertext: doc.dynInputCiphertext,
algorithm: doc.dynAlgorithm,
keyEncoding: doc.dynKeyEncoding,
folderId: doc.dynFolderId, folderId: doc.dynFolderId,
status: doc.dynStatus, status: doc.dynStatus,
statusDetails: doc.dynStatusDetails, statusDetails: doc.dynStatusDetails,
createdAt: doc.dynCreatedAt, createdAt: doc.dynCreatedAt,
updatedAt: doc.dynUpdatedAt updatedAt: doc.dynUpdatedAt,
encryptedConfig: doc.dynEncryptedConfig
} }
}; };
} catch (error) { } catch (error) {
@@ -1,8 +1,9 @@
import { SecretKeyEncoding } from "@app/db/schemas";
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
@@ -14,6 +15,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">; dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">; dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>; dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
}; };
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
@@ -22,7 +25,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService, queueService,
dynamicSecretDAL, dynamicSecretDAL,
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretLeaseDAL dynamicSecretLeaseDAL,
kmsService,
folderDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => { }: TDynamicSecretLeaseQueueServiceFactoryDep) => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue( await queueService.queue(
@@ -77,15 +82,20 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
const { projectId } = folder;
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
@@ -100,17 +110,22 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
throw new DisableRotationErrors({ message: "Document not deleted" }); throw new DisableRotationErrors({ message: "Document not deleted" });
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
const { projectId } = folder;
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
if (dynamicSecretLeases.length) { if (dynamicSecretLeases.length) {
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ const dynamicSecretInputConfig = secretManagerDecryptor({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, cipherTextBlob: dynamicSecretCfg.encryptedConfig
ciphertext: dynamicSecretCfg.inputCiphertext, }).toString();
tag: dynamicSecretCfg.inputTag, const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
iv: dynamicSecretCfg.inputIV
})
) as object;
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
await Promise.all( await Promise.all(
@@ -1,14 +1,14 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -34,6 +34,7 @@ type TDynamicSecretLeaseServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>; export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
@@ -46,7 +47,8 @@ export const dynamicSecretLeaseServiceFactory = ({
permissionService, permissionService,
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
licenseService licenseService,
kmsService
}: TDynamicSecretLeaseServiceFactoryDep) => { }: TDynamicSecretLeaseServiceFactoryDep) => {
const create = async ({ const create = async ({
environmentSlug, environmentSlug,
@@ -94,14 +96,12 @@ export const dynamicSecretLeaseServiceFactory = ({
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
infisicalSymmetricDecrypt({ type: KmsDataKey.SecretManager,
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, projectId
ciphertext: dynamicSecretCfg.inputCiphertext, });
tag: dynamicSecretCfg.inputTag, const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
iv: dynamicSecretCfg.inputIV const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg; const { maxTTL } = dynamicSecretCfg;
@@ -164,14 +164,12 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
infisicalSymmetricDecrypt({ type: KmsDataKey.SecretManager,
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, projectId
ciphertext: dynamicSecretCfg.inputCiphertext, });
tag: dynamicSecretCfg.inputTag, const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
iv: dynamicSecretCfg.inputIV const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg; const { maxTTL } = dynamicSecretCfg;
@@ -231,14 +229,12 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
infisicalSymmetricDecrypt({ type: KmsDataKey.SecretManager,
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, projectId
ciphertext: dynamicSecretCfg.inputCiphertext, });
tag: dynamicSecretCfg.inputTag, const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
iv: dynamicSecretCfg.inputIV const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
})
) as object;
const revokeResponse = await selectedProvider const revokeResponse = await selectedProvider
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
@@ -1,11 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -34,6 +34,7 @@ type TDynamicSecretServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>; export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -46,7 +47,8 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretProviders, dynamicSecretProviders,
permissionService, permissionService,
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL projectDAL,
kmsService
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
path, path,
@@ -96,16 +98,16 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob;
const dynamicSecretCfg = await dynamicSecretDAL.create({ const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type, type: provider.type,
version: 1, version: 1,
inputIV: encryptedInput.iv, encryptedConfig,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
@@ -165,27 +167,28 @@ export const dynamicSecretServiceFactory = ({
} }
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
infisicalSymmetricDecrypt({ await kmsService.createCipherPairWithDataKey({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, type: KmsDataKey.SecretManager,
ciphertext: dynamicSecretCfg.inputCiphertext, projectId
tag: dynamicSecretCfg.inputTag, });
iv: dynamicSecretCfg.inputIV const dynamicSecretInputConfig = secretManagerDecryptor({
}) cipherTextBlob: dynamicSecretCfg.encryptedConfig
) as object; }).toString();
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput)); const encryptedConfig = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(updatedInput))
}).cipherTextBlob;
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
inputIV: encryptedInput.iv, encryptedConfig,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
@@ -286,14 +289,16 @@ export const dynamicSecretServiceFactory = ({
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
const decryptedStoredInput = JSON.parse( const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
infisicalSymmetricDecrypt({ type: KmsDataKey.SecretManager,
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, projectId
ciphertext: dynamicSecretCfg.inputCiphertext, });
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV const dynamicSecretInputConfig = secretManagerDecryptor({
}) cipherTextBlob: dynamicSecretCfg.encryptedConfig
) as object; }).toString();
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
return { ...dynamicSecretCfg, inputs: providerInputs }; return { ...dynamicSecretCfg, inputs: providerInputs };
@@ -31,6 +31,8 @@ export const externalKmsDALFactory = (db: TDbClient) => {
isReserved: el.isReserved, isReserved: el.isReserved,
orgId: el.orgId, orgId: el.orgId,
slug: el.slug, slug: el.slug,
createdAt: el.createdAt,
updatedAt: el.updatedAt,
externalKms: { externalKms: {
id: el.externalKmsId, id: el.externalKmsId,
provider: el.externalKmsProvider, provider: el.externalKmsProvider,
@@ -5,7 +5,9 @@ import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TExternalKmsDALFactory } from "./external-kms-dal"; import { TExternalKmsDALFactory } from "./external-kms-dal";
@@ -22,9 +24,10 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
type TExternalKmsServiceFactoryDep = { type TExternalKmsServiceFactoryDep = {
externalKmsDAL: TExternalKmsDALFactory; externalKmsDAL: TExternalKmsDALFactory;
kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "encryptWithKmsKey" | "decryptWithKmsKey">; kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "createCipherPairWithDataKey">;
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">; kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>; export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>;
@@ -32,6 +35,7 @@ export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFac
export const externalKmsServiceFactory = ({ export const externalKmsServiceFactory = ({
externalKmsDAL, externalKmsDAL,
permissionService, permissionService,
licenseService,
kmsService, kmsService,
kmsDAL kmsDAL
}: TExternalKmsServiceFactoryDep) => { }: TExternalKmsServiceFactoryDep) => {
@@ -51,7 +55,15 @@ export const externalKmsServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to create external KMS due to plan restriction. Upgrade to the Enterprise plan."
});
}
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
let sanitizedProviderInput = ""; let sanitizedProviderInput = "";
@@ -59,21 +71,23 @@ export const externalKmsServiceFactory = ({
case KmsProviders.Aws: case KmsProviders.Aws:
{ {
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }); const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
await externalKms.validateConnection();
// if missing kms key this generate a new kms key id and returns new provider input // if missing kms key this generate a new kms key id and returns new provider input
const newProviderInput = await externalKms.generateInputKmsKey(); const newProviderInput = await externalKms.generateInputKmsKey();
sanitizedProviderInput = JSON.stringify(newProviderInput); sanitizedProviderInput = JSON.stringify(newProviderInput);
await externalKms.validateConnection();
} }
break; break;
default: default:
throw new BadRequestError({ message: "external kms provided is invalid" }); throw new BadRequestError({ message: "external kms provided is invalid" });
} }
const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId); const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
const kmsEncryptor = await kmsService.encryptWithKmsKey({ type: KmsDataKey.Organization,
kmsId: orgKmsKeyId orgId: actorOrgId
}); });
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({
const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8") plainText: Buffer.from(sanitizedProviderInput, "utf8")
}); });
@@ -119,19 +133,28 @@ export const externalKmsServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
const plan = await licenseService.getPlan(kmsDoc.orgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to update external KMS due to plan restriction. Upgrade to the Enterprise plan."
});
}
const kmsSlug = slug ? slugify(slug) : undefined; const kmsSlug = slug ? slugify(slug) : undefined;
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId);
let sanitizedProviderInput = ""; let sanitizedProviderInput = "";
if (provider) { const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
const kmsDecryptor = await kmsService.decryptWithKmsKey({ await kmsService.createCipherPairWithDataKey({
kmsId: orgDefaultKmsId type: KmsDataKey.Organization,
orgId: actorOrgId
}); });
const decryptedProviderInputBlob = kmsDecryptor({ if (provider) {
const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs cipherTextBlob: externalKmsDoc.encryptedProviderInputs
}); });
@@ -154,10 +177,7 @@ export const externalKmsServiceFactory = ({
let encryptedProviderInputs: Buffer | undefined; let encryptedProviderInputs: Buffer | undefined;
if (sanitizedProviderInput) { if (sanitizedProviderInput) {
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const { cipherTextBlob } = orgDataKeyEncryptor({
kmsId: orgDefaultKmsId
});
const { cipherTextBlob } = kmsEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8") plainText: Buffer.from(sanitizedProviderInput, "utf8")
}); });
encryptedProviderInputs = cipherTextBlob; encryptedProviderInputs = cipherTextBlob;
@@ -197,7 +217,7 @@ export const externalKmsServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
@@ -218,7 +238,7 @@ export const externalKmsServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId }); const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
@@ -234,16 +254,18 @@ export const externalKmsServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ type: KmsDataKey.Organization,
kmsId: orgDefaultKmsId orgId: actorOrgId
}); });
const decryptedProviderInputBlob = kmsDecryptor({
const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs cipherTextBlob: externalKmsDoc.encryptedProviderInputs
}); });
switch (externalKmsDoc.provider) { switch (externalKmsDoc.provider) {
@@ -273,16 +295,17 @@ export const externalKmsServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ type: KmsDataKey.Organization,
kmsId: orgDefaultKmsId orgId: actorOrgId
}); });
const decryptedProviderInputBlob = kmsDecryptor({
const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs cipherTextBlob: externalKmsDoc.encryptedProviderInputs
}); });
@@ -50,17 +50,26 @@ type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & {
}; };
export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => { export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => {
const providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs); let providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
const awsClient = await getAwsKmsClient(providerInputs); let awsClient = await getAwsKmsClient(providerInputs);
const generateInputKmsKey = async () => { const generateInputKmsKey = async () => {
if (providerInputs.kmsKeyId) return providerInputs; if (providerInputs.kmsKeyId) return providerInputs;
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] }); const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
const kmsKey = await awsClient.send(command); const kmsKey = await awsClient.send(command);
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key"); if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
return { ...providerInputs, kmsKeyId: kmsKey.KeyMetadata?.KeyId }; const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({
...providerInputs,
kmsKeyId: kmsKey.KeyMetadata?.KeyId
});
providerInputs = updatedProviderInputs;
awsClient = await getAwsKmsClient(providerInputs);
return updatedProviderInputs;
}; };
const validateConnection = async () => { const validateConnection = async () => {
@@ -39,7 +39,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretApproval: false, secretApproval: false,
secretRotation: true, secretRotation: true,
caCrl: false, caCrl: false,
instanceUserManagement: false instanceUserManagement: false,
externalKms: false
}); });
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -57,6 +57,7 @@ export type TFeatureSet = {
secretRotation: true; secretRotation: true;
caCrl: false; caCrl: false;
instanceUserManagement: false; instanceUserManagement: false;
externalKms: false;
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {
@@ -21,7 +21,8 @@ export enum OrgPermissionSubjects {
Groups = "groups", Groups = "groups",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity" Identity = "identity",
Kms = "kms"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@@ -37,7 +38,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]; | [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@@ -100,6 +102,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
return build({ conditionsMatcher }); return build({ conditionsMatcher });
}; };
@@ -28,7 +28,8 @@ export enum ProjectPermissionSub {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
Identity = "identity", Identity = "identity",
CertificateAuthorities = "certificate-authorities", CertificateAuthorities = "certificate-authorities",
Certificates = "certificates" Certificates = "certificates",
Kms = "kms"
} }
type SubjectFields = { type SubjectFields = {
@@ -60,7 +61,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
const buildAdminPermissionRules = () => { const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
@@ -157,6 +159,8 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
return rules; return rules;
}; };
@@ -356,5 +356,161 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
} }
}; };
return { ...secretApprovalRequestOrm, findById, findProjectRequestCount, findByProjectId }; const findByProjectIdBridgeSecretV2 = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
tx?: Knex
) => {
try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at.
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.join(
TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id`
)
.leftJoin(
TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`,
`${TableName.SecretApprovalRequestReviewer}.requestId`
)
.leftJoin<TSecretApprovalRequestsSecrets>(
TableName.SecretApprovalRequestSecretV2,
`${TableName.SecretApprovalRequestSecretV2}.requestId`,
`${TableName.SecretApprovalRequest}.id`
)
.where(
stripUndefinedInWhere({
projectId,
[`${TableName.Environment}.slug` as "slug"]: environment,
[`${TableName.SecretApprovalRequest}.status`]: status,
committerUserId: committer
})
)
.andWhere(
(bd) =>
void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
)
.select(selectAllTableCols(TableName.SecretApprovalRequest))
.select(
db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("environment"),
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"),
db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
db.ref("op").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitOp"),
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitSecretId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitId"),
db.raw(
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
),
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
)
.orderBy("createdAt", "desc");
const docs = await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit);
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({
...SecretApprovalRequestsSchema.parse(el),
environment: el.environment,
projectId: el.projectId,
policy: {
id: el.policyId,
name: el.policyName,
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel
},
committerUser: {
userId: el.committerUserId,
email: el.committerUserEmail,
firstName: el.committerUserFirstName,
lastName: el.committerUserLastName,
username: el.committerUserUsername
}
}),
childrenMapper: [
{
key: "reviewerId",
label: "reviewers" as const,
mapper: ({ reviewerUserId, reviewerStatus: s }) =>
reviewerUserId ? { userId: reviewerUserId, status: s } : undefined
},
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId }) => approverUserId
},
{
key: "commitId",
label: "commits" as const,
mapper: ({ commitSecretId: secretId, commitId: id, commitOp: op }) => ({
op,
id,
secretId
})
}
]
});
return formatedDoc.map((el) => ({
...el,
policy: { ...el.policy, approvers: el.approvers }
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindSAR" });
}
};
const deleteByProjectId = async (projectId: string, tx?: Knex) => {
try {
const query = await (tx || db)(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where({ projectId })
.delete();
return query;
} catch (error) {
throw new DatabaseError({ error, name: "DeleteByProjectId" });
}
};
return {
...secretApprovalRequestOrm,
findById,
findProjectRequestCount,
findByProjectId,
findByProjectIdBridgeSecretV2,
deleteByProjectId
};
}; };
@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { import {
SecretApprovalRequestsSecretsSchema, SecretApprovalRequestsSecretsSchema,
SecretApprovalRequestsSecretsV2Schema,
TableName, TableName,
TSecretApprovalRequestsSecrets, TSecretApprovalRequestsSecrets,
TSecretTags TSecretTags
@@ -15,6 +16,8 @@ export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApp
export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
const secretApprovalRequestSecretV2TagOrm = ormify(db, TableName.SecretApprovalRequestSecretTagV2);
const secretApprovalRequestSecretV2Orm = ormify(db, TableName.SecretApprovalRequestSecretV2);
const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => {
try { try {
@@ -221,10 +224,197 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
throw new DatabaseError({ error, name: "FindByRequestId" }); throw new DatabaseError({ error, name: "FindByRequestId" });
} }
}; };
const findByRequestIdBridgeSecretV2 = async (requestId: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())({
secVerTag: TableName.SecretTag
})
.from(TableName.SecretApprovalRequestSecretV2)
.where({ requestId })
.leftJoin(
TableName.SecretApprovalRequestSecretTagV2,
`${TableName.SecretApprovalRequestSecretV2}.id`,
`${TableName.SecretApprovalRequestSecretTagV2}.secretId`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretApprovalRequestSecretTagV2}.tagId`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.SecretV2, `${TableName.SecretApprovalRequestSecretV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretVersionV2,
`${TableName.SecretVersionV2}.id`,
`${TableName.SecretApprovalRequestSecretV2}.secretVersion`
)
.leftJoin(
TableName.SecretVersionV2Tag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin<TSecretTags>(
db.ref(TableName.SecretTag).as("secVerTag"),
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
db.ref("id").withSchema("secVerTag")
)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({
secVerTagId: "secVerTag.id",
secVerTagColor: "secVerTag.color",
secVerTagSlug: "secVerTag.slug",
secVerTagName: "secVerTag.name"
})
.select(
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
)
.select(
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
db.ref("key").withSchema(TableName.SecretV2).as("orgSecKey"),
db.ref("encryptedValue").withSchema(TableName.SecretV2).as("orgSecValue"),
db.ref("encryptedComment").withSchema(TableName.SecretV2).as("orgSecComment")
)
.select(
db.ref("version").withSchema(TableName.SecretVersionV2).as("secVerVersion"),
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
);
const formatedDoc = sqlNestRelationships({
data: doc,
key: "id",
parentMapper: (data) => SecretApprovalRequestsSecretsV2Schema.omit({ secretVersion: true }).parse(data),
childrenMapper: [
{
key: "tagJnId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
id,
name,
slug,
color
})
},
{
key: "secretId",
label: "secret" as const,
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) =>
secretId
? {
id: secretId,
version: orgSecVersion,
key: orgSecKey,
encryptedValue: orgSecValue,
encryptedComment: orgSecComment
}
: undefined
},
{
key: "secretVersion",
label: "secretVersion" as const,
mapper: ({ secretVersion, secVerVersion, secVerKey, secVerValue, secVerComment }) =>
secretVersion
? {
version: secVerVersion,
id: secretVersion,
key: secVerKey,
encryptedValue: secVerValue,
encryptedComment: secVerComment
}
: undefined,
childrenMapper: [
{
key: "secVerTagId",
label: "tags" as const,
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
// eslint-disable-next-line
id,
// eslint-disable-next-line
name,
// eslint-disable-next-line
slug,
// eslint-disable-next-line
color
})
}
]
}
]
});
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el,
secret: secret?.[0],
secretVersion: secretVersion?.[0]
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindByRequestId" });
}
};
// special query for migration to v2 secret
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretApprovalRequestSecret)
.join(
TableName.SecretApprovalRequest,
`${TableName.SecretApprovalRequest}.id`,
`${TableName.SecretApprovalRequestSecret}.requestId`
)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.SecretApprovalRequestSecretTag,
`${TableName.SecretApprovalRequestSecret}.id`,
`${TableName.SecretApprovalRequestSecretTag}.secretId`
)
.where({ projectId })
.select(selectAllTableCols(TableName.SecretApprovalRequestSecret))
.select(
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagId"),
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagSecretId"),
db.ref("tagId").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagSecretTagId"),
db.ref("createdAt").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagCreatedAt"),
db.ref("updatedAt").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagUpdatedAt")
);
const formatedDoc = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => SecretApprovalRequestsSecretsSchema.parse(data),
childrenMapper: [
{
key: "secretApprovalTagId",
label: "tags" as const,
mapper: ({
secretApprovalTagSecretId,
secretApprovalTagId,
secretApprovalTagUpdatedAt,
secretApprovalTagCreatedAt
}) => ({
secretApprovalTagSecretId,
secretApprovalTagId,
secretApprovalTagUpdatedAt,
secretApprovalTagCreatedAt
})
}
]
});
return formatedDoc;
} catch (error) {
throw new DatabaseError({ error, name: "FindByRequestId" });
}
};
return { return {
...secretApprovalRequestSecretOrm, ...secretApprovalRequestSecretOrm,
insertV2Bridge: secretApprovalRequestSecretV2Orm.insertMany,
findByRequestId, findByRequestId,
findByRequestIdBridgeSecretV2,
bulkUpdateNoVersionIncrement, bulkUpdateNoVersionIncrement,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany findByProjectId,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany,
insertApprovalSecretV2Tags: secretApprovalRequestSecretV2TagOrm.insertMany
}; };
}; };
@@ -5,20 +5,25 @@ import {
SecretEncryptionAlgo, SecretEncryptionAlgo,
SecretKeyEncoding, SecretKeyEncoding,
SecretType, SecretType,
TSecretApprovalRequestsSecretsInsert TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsV2Insert
} from "@app/db/schemas"; } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn"; import { groupBy, pick, unique } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/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 { KmsDataKey } from "@app/services/kms/kms-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";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { import {
decryptSecretWithBot,
fnSecretBlindIndexCheck, fnSecretBlindIndexCheck,
fnSecretBlindIndexCheckV2, fnSecretBlindIndexCheckV2,
fnSecretBulkDelete, fnSecretBulkDelete,
@@ -33,6 +38,15 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import {
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -47,6 +61,7 @@ import {
RequestState, RequestState,
TApprovalRequestCountDTO, TApprovalRequestCountDTO,
TGenerateSecretApprovalRequestDTO, TGenerateSecretApprovalRequestDTO,
TGenerateSecretApprovalRequestV2BridgeDTO,
TListApprovalsDTO, TListApprovalsDTO,
TMergeSecretApprovalRequestDTO, TMergeSecretApprovalRequestDTO,
TReviewRequestDTO, TReviewRequestDTO,
@@ -62,16 +77,26 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory; secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findSecretPathByFolderIds">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findSecretPathByFolderIds">;
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret">; secretTagDAL: Pick<
TSecretTagDALFactory,
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
>;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">; secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectById">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne">; userDAL: Pick<TUserDALFactory, "find" | "findOne">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
}; };
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>; export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -93,7 +118,11 @@ export const secretApprovalRequestServiceFactory = ({
projectBotService, projectBotService,
smtpService, smtpService,
userDAL, userDAL,
projectEnvDAL projectEnvDAL,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@@ -125,6 +154,19 @@ export const secretApprovalRequestServiceFactory = ({
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
projectId,
committer,
environment,
status,
userId: actorId,
limit,
offset
});
}
const approvals = await secretApprovalRequestDAL.findByProjectId({ const approvals = await secretApprovalRequestDAL.findByProjectId({
projectId, projectId,
committer, committer,
@@ -149,11 +191,14 @@ export const secretApprovalRequestServiceFactory = ({
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id); const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
const { projectId } = secretApprovalRequest;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const { policy } = secretApprovalRequest; const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
secretApprovalRequest.projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
@@ -165,7 +210,75 @@ export const secretApprovalRequestServiceFactory = ({
throw new UnauthorizedError({ message: "User has no access" }); throw new UnauthorizedError({ message: "User has no access" });
} }
const secrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); let secrets;
if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretApprovalRequest.id
);
secrets = encrypedSecrets.map((el) => ({
...el,
secretKey: el.key,
id: el.id,
version: el.version,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined,
secret: el.secret
? {
secretKey: el.secret.key,
id: el.secret.id,
version: el.secret.version,
secretValue: el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
: undefined,
secretComment: el.secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
: undefined
}
: undefined,
secretVersion: el.secretVersion
? {
secretKey: el.secretVersion.key,
id: el.secretVersion.id,
version: el.secretVersion.version,
secretValue: el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
: undefined,
secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: undefined
}
: undefined
}));
} else {
if (!botKey) throw new BadRequestError({ message: "Bot key not found" });
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encrypedSecrets.map((el) => ({
...el,
...decryptSecretWithBot(el, botKey),
secret: el.secret
? {
id: el.secret.id,
version: el.secret.version,
...decryptSecretWithBot(el.secret, botKey)
}
: undefined,
secretVersion: el.secretVersion
? {
id: el.secretVersion.id,
version: el.secretVersion.version,
...decryptSecretWithBot(el.secretVersion, botKey)
}
: undefined
}));
}
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [ const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
secretApprovalRequest.folderId secretApprovalRequest.folderId
]); ]);
@@ -300,48 +413,167 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalRequest.policy.approvers.filter( secretApprovalRequest.policy.approvers.filter(
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED ({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length; ).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
if (!hasMinApproval && !isSoftEnforcement) if (!hasMinApproval && !isSoftEnforcement)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
const conflicts: Array<{ secretId: string; op: SecretOperations }> = []; const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create); let mergeStatus;
if (secretCreationCommits.length) { if (shouldUseSecretV2Bridge) {
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({ // this cycle if for bridged secrets
folderId, const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretDAL, secretApprovalRequest.id
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex };
})
});
secretCreationCommits
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
});
secretCreationCommits = secretCreationCommits.filter(
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
); );
} if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
if (secretUpdationCommits.length) { type: KmsDataKey.SecretManager,
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({ projectId
folderId, });
secretDAL,
userId: "", const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
inputSecrets: secretUpdationCommits let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex) if (secretCreationCommits.length) {
.map(({ secretBlindIndex }) => { const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretCreationCommits.map((el) => ({
key: el.key,
type: SecretType.Shared
}))
);
const creationConflictSecretsGroupByKey = groupBy(secrets, (i) => i.key);
secretCreationCommits
.filter(({ key }) => creationConflictSecretsGroupByKey[key])
.forEach((el) => {
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
});
secretCreationCommits = secretCreationCommits.filter(({ key }) => !creationConflictSecretsGroupByKey[key]);
}
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
if (secretUpdationCommits.length) {
const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretCreationCommits.map((el) => ({
key: el.key,
type: SecretType.Shared
}))
);
const updationConflictSecretsGroupByKey = groupBy(secrets, (i) => i.key);
secretUpdationCommits
.filter(({ key, secretId }) => updationConflictSecretsGroupByKey[key] || !secretId)
.forEach((el) => {
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
});
secretUpdationCommits = secretUpdationCommits.filter(
({ key, secretId }) => Boolean(secretId) && !updationConflictSecretsGroupByKey[key]
);
}
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
const newSecrets = secretCreationCommits.length
? await fnSecretV2BridgeBulkInsert({
tx,
folderId,
inputSecrets: secretCreationCommits.map((el) => ({
tagIds: el?.tags.map(({ id }) => id),
version: 1,
encryptedComment: el.encryptedComment,
encryptedValue: el.encryptedValue,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()
)
: [],
type: SecretType.Shared
})),
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
})
: [];
const updatedSecrets = secretUpdationCommits.length
? await fnSecretV2BridgeBulkUpdate({
folderId,
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
typeof el.encryptedValue !== "undefined"
? {
encryptedValue: el.encryptedValue as Buffer,
references: el.encryptedValue
? getAllNestedSecretReferencesV2Bridge(
secretManagerDecryptor({
cipherTextBlob: el.encryptedValue
}).toString()
)
: []
}
: {};
return {
filter: { id: el.secretId as string, type: SecretType.Shared },
data: {
reminderRepeatDays: el.reminderRepeatDays,
encryptedComment: el.encryptedComment,
reminderNote: el.reminderNote,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
tagIds: el?.tags.map(({ id }) => id),
...encryptedValue
}
};
}),
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
? await fnSecretV2BridgeBulkDelete({
projectId,
folderId,
tx,
actorId: "",
secretDAL: secretV2BridgeDAL,
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
})
: [];
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
secretApprovalRequest.id,
{
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangedByUserId: actorId
},
tx
);
return {
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
approval: updatedSecretApproval
};
});
} else {
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
if (secretCreationCommits.length) {
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
folderId,
secretDAL,
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) { if (!secretBlindIndex) {
throw new BadRequestError({ throw new BadRequestError({
message: "Missing secret blind index" message: "Missing secret blind index"
@@ -349,80 +581,56 @@ export const secretApprovalRequestServiceFactory = ({
} }
return { secretBlindIndex }; return { secretBlindIndex };
}) })
});
secretUpdationCommits
.filter(
({ secretBlindIndex, secretId }) =>
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
)
.forEach((el) => {
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
}); });
secretCreationCommits
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
});
secretCreationCommits = secretCreationCommits.filter(
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
);
}
secretUpdationCommits = secretUpdationCommits.filter( let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
({ secretBlindIndex, secretId }) => if (secretUpdationCommits.length) {
Boolean(secretId) && (secretBlindIndex ? !conflictGroupByBlindIndex[secretBlindIndex] : true) const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
); folderId,
} secretDAL,
userId: "",
inputSecrets: secretUpdationCommits
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex };
})
});
secretUpdationCommits
.filter(
({ secretBlindIndex, secretId }) =>
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
)
.forEach((el) => {
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
});
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete); secretUpdationCommits = secretUpdationCommits.filter(
const botKey = await projectBotService.getBotKey(projectId).catch(() => null); ({ secretBlindIndex, secretId }) =>
const mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => { Boolean(secretId) && (secretBlindIndex ? !conflictGroupByBlindIndex[secretBlindIndex] : true)
const newSecrets = secretCreationCommits.length );
? await fnSecretBulkInsert({ }
tx,
folderId, const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
inputSecrets: secretCreationCommits.map((el) => ({ mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
...pick(el, [ const newSecrets = secretCreationCommits.length
"secretCommentCiphertext", ? await fnSecretBulkInsert({
"secretCommentTag", tx,
"secretCommentIV", folderId,
"secretValueIV", inputSecrets: secretCreationCommits.map((el) => ({
"secretValueTag",
"secretValueCiphertext",
"secretKeyCiphertext",
"secretKeyTag",
"secretKeyIV",
"metadata",
"skipMultilineEncoding",
"secretReminderNote",
"secretReminderRepeatDays",
"algorithm",
"keyEncoding",
"secretBlindIndex"
]),
tags: el?.tags.map(({ id }) => id),
version: 1,
type: SecretType.Shared,
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
: [];
const updatedSecrets = secretUpdationCommits.length
? await fnSecretBulkUpdate({
folderId,
projectId,
tx,
inputSecrets: secretUpdationCommits.map((el) => ({
filter: {
id: el.secretId as string, // this null check is already checked at top on conflict strategy
type: SecretType.Shared
},
data: {
tags: el?.tags.map(({ id }) => id),
...pick(el, [ ...pick(el, [
"secretCommentCiphertext", "secretCommentCiphertext",
"secretCommentTag", "secretCommentTag",
@@ -437,8 +645,13 @@ export const secretApprovalRequestServiceFactory = ({
"skipMultilineEncoding", "skipMultilineEncoding",
"secretReminderNote", "secretReminderNote",
"secretReminderRepeatDays", "secretReminderRepeatDays",
"algorithm",
"keyEncoding",
"secretBlindIndex" "secretBlindIndex"
]), ]),
tags: el?.tags.map(({ id }) => id),
version: 1,
type: SecretType.Shared,
references: botKey references: botKey
? getAllNestedSecretReferences( ? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({ decryptSymmetric128BitHexKeyUTF8({
@@ -449,48 +662,94 @@ export const secretApprovalRequestServiceFactory = ({
}) })
) )
: undefined : undefined
} })),
})), secretDAL,
secretDAL, secretVersionDAL,
secretVersionDAL, secretTagDAL,
secretTagDAL, secretVersionTagDAL
secretVersionTagDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
? await fnSecretBulkDelete({
projectId,
folderId,
tx,
actorId: "",
secretDAL,
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex, type: SecretType.Shared };
}) })
}) : [];
: []; const updatedSecrets = secretUpdationCommits.length
const updatedSecretApproval = await secretApprovalRequestDAL.updateById( ? await fnSecretBulkUpdate({
secretApprovalRequest.id, folderId,
{ projectId,
conflicts: JSON.stringify(conflicts), tx,
hasMerged: true, inputSecrets: secretUpdationCommits.map((el) => ({
status: RequestState.Closed, filter: {
statusChangedByUserId: actorId, id: el.secretId as string, // this null check is already checked at top on conflict strategy
bypassReason type: SecretType.Shared
}, },
tx data: {
); tags: el?.tags.map(({ id }) => id),
return { ...pick(el, [
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret }, "secretCommentCiphertext",
approval: updatedSecretApproval "secretCommentTag",
}; "secretCommentIV",
}); "secretValueIV",
"secretValueTag",
"secretValueCiphertext",
"secretKeyCiphertext",
"secretKeyTag",
"secretKeyIV",
"metadata",
"skipMultilineEncoding",
"secretReminderNote",
"secretReminderRepeatDays",
"secretBlindIndex"
]),
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
}
})),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
? await fnSecretBulkDelete({
projectId,
folderId,
tx,
actorId: "",
secretDAL,
secretQueueService,
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
message: "Missing secret blind index"
});
}
return { secretBlindIndex, type: SecretType.Shared };
})
})
: [];
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
secretApprovalRequest.id,
{
conflicts: JSON.stringify(conflicts),
hasMerged: true,
status: RequestState.Closed,
statusChangedByUserId: actorId
},
tx
);
return {
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
approval: updatedSecretApproval
};
});
}
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]); const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -779,8 +1038,262 @@ export const secretApprovalRequestServiceFactory = ({
}); });
return secretApprovalRequest; return secretApprovalRequest;
}; };
const generateSecretApprovalRequestV2Bridge = async ({
data,
actorId,
actor,
actorOrgId,
actorAuthMethod,
policy,
projectId,
secretPath,
environment
}: TGenerateSecretApprovalRequestV2BridgeDTO) => {
if (actor === ActorType.SERVICE || actor === ActorType.Machine)
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new BadRequestError({
message: "Folder not found for the given environment slug & secret path",
name: "GenSecretApproval"
});
const folderId = folder.id;
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
const commitTagIds: Record<string, string[]> = {};
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
// for created secret approval change
const createdSecrets = data[SecretOperations.Create];
if (createdSecrets && createdSecrets?.length) {
const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
createdSecrets.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
commits.push(
...createdSecrets.map((createdSecret) => ({
op: SecretOperations.Create,
version: 1,
encryptedComment: setKnexStringValue(
createdSecret.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
encryptedValue: setKnexStringValue(
createdSecret.secretValue,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
key: createdSecret.secretKey,
type: SecretType.Shared
}))
);
createdSecrets.forEach(({ tagIds, secretKey }) => {
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
});
}
// not secret approval for update operations
const secretsToUpdate = data[SecretOperations.Update];
if (secretsToUpdate && secretsToUpdate?.length) {
const secretsToUpdateStoredInDB = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretsToUpdate.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
});
// now find any secret that needs to update its name
// same process as above
const secretsWithNewName = secretsToUpdate.filter(({ newSecretName }) => Boolean(newSecretName));
if (secretsWithNewName.length) {
const secrets = await secretV2BridgeDAL.findBySecretKeys(
folderId,
secretsWithNewName.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secrets.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
});
}
const updatingSecretsGroupByKey = groupBy(secretsToUpdateStoredInDB, (el) => el.key);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(
folderId,
secretsToUpdateStoredInDB.map(({ id }) => id)
);
commits.push(
...secretsToUpdate.map(
({
newSecretName,
secretKey,
tagIds,
secretValue,
reminderRepeatDays,
reminderNote,
secretComment,
metadata,
skipMultilineEncoding
}) => {
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
return {
...latestSecretVersions[secretId],
key: newSecretName || secretKey,
encryptedComment: setKnexStringValue(
secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
encryptedValue: setKnexStringValue(
secretValue,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
reminderRepeatDays,
reminderNote,
metadata,
skipMultilineEncoding,
op: SecretOperations.Update as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId].id,
version: updatingSecretsGroupByKey[secretKey][0].version || 1
};
}
)
);
}
// deleted secrets
const deletedSecrets = data[SecretOperations.Delete];
if (deletedSecrets && deletedSecrets.length) {
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys(
folderId,
deletedSecrets.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
}))
);
if (secretsToDeleteInDB.length !== deletedSecrets.length)
throw new BadRequestError({
message: `Secret not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
});
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
commits.push(
...deletedSecrets.map(({ secretKey }) => {
const secretId = secretsGroupedByKey[secretKey][0].id;
return {
op: SecretOperations.Delete as const,
...latestSecretVersions[secretId],
key: secretKey,
secret: secretId,
secretVersion: latestSecretVersions[secretId].id
};
})
);
}
if (!commits.length) throw new BadRequestError({ message: "Empty commits" });
const tagIds = unique(Object.values(commitTagIds).flat());
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
const doc = await secretApprovalRequestDAL.create(
{
folderId,
slug: alphaNumericNanoId(),
policyId: policy.id,
status: "open",
hasMerged: false,
committerUserId: actorId
},
tx
);
const approvalCommits = await secretApprovalRequestSecretDAL.insertV2Bridge(
commits.map(
({
version,
op,
key,
encryptedComment,
skipMultilineEncoding,
metadata,
reminderNote,
reminderRepeatDays,
encryptedValue,
secretId,
secretVersion
}) => ({
version,
requestId: doc.id,
op,
secretId,
metadata,
secretVersion,
skipMultilineEncoding,
encryptedValue,
reminderRepeatDays,
reminderNote,
encryptedComment,
key
})
),
tx
);
const commitsGroupByKey = groupBy(approvalCommits, (i) => i.key);
if (tagIds.length) {
await secretApprovalRequestSecretDAL.insertApprovalSecretV2Tags(
Object.keys(commitTagIds).flatMap((blindIndex) =>
commitTagIds[blindIndex]
? commitTagIds[blindIndex].map((tagId) => ({
secretId: commitsGroupByKey[blindIndex][0].id,
tagId
}))
: []
),
tx
);
}
return { ...doc, commits: approvalCommits };
});
return secretApprovalRequest;
};
return { return {
generateSecretApprovalRequest, generateSecretApprovalRequest,
generateSecretApprovalRequestV2Bridge,
mergeSecretApprovalRequest, mergeSecretApprovalRequest,
reviewApproval, reviewApproval,
updateApprovalStatus, updateApprovalStatus,
@@ -26,6 +26,23 @@ export type TApprovalUpdateSecret = Partial<TApprovalCreateSecret> & {
tagIds?: string[]; tagIds?: string[];
}; };
export type TApprovalCreateSecretV2Bridge = {
secretKey: string;
secretValue?: string;
secretComment?: string;
reminderNote?: string | null;
reminderRepeatDays?: number | null;
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
tagIds?: string[];
};
export type TApprovalUpdateSecretV2Bridge = Partial<TApprovalCreateSecretV2Bridge> & {
secretKey: string;
newSecretName?: string;
tagIds?: string[];
};
export type TGenerateSecretApprovalRequestDTO = { export type TGenerateSecretApprovalRequestDTO = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -37,6 +54,17 @@ export type TGenerateSecretApprovalRequestDTO = {
}; };
} & TProjectPermission; } & TProjectPermission;
export type TGenerateSecretApprovalRequestV2BridgeDTO = {
environment: string;
secretPath: string;
policy: TSecretApprovalPolicies;
data: {
[SecretOperations.Create]?: TApprovalCreateSecretV2Bridge[];
[SecretOperations.Update]?: TApprovalUpdateSecretV2Bridge[];
[SecretOperations.Delete]?: { secretKey: string }[];
};
} & TProjectPermission;
export type TMergeSecretApprovalRequestDTO = { export type TMergeSecretApprovalRequestDTO = {
approvalId: string; approvalId: string;
bypassReason?: string; bypassReason?: string;
@@ -1,4 +1,4 @@
import { SecretType, TSecrets } from "@app/db/schemas"; import { SecretType, TSecrets, TSecretsV2 } from "@app/db/schemas";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
@@ -10,6 +10,8 @@ import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueName, TQueueServiceFactory } from "@app/queue";
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 { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns"; import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
@@ -17,12 +19,20 @@ import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/
import { SecretOperations } from "@app/services/secret/secret-types"; import { SecretOperations } from "@app/services/secret/secret-types";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { ReservedFolders } from "@app/services/secret-folder/secret-folder-types"; import { ReservedFolders } from "@app/services/secret-folder/secret-folder-types";
import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal"; import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
import { fnSecretsFromImports } from "@app/services/secret-import/secret-import-fns"; import { fnSecretsFromImports, fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import {
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
getAllNestedSecretReferences,
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { MAX_REPLICATION_DEPTH } from "./secret-replication-constants"; import { MAX_REPLICATION_DEPTH } from "./secret-replication-constants";
@@ -32,24 +42,42 @@ type TSecretReplicationServiceFactoryDep = {
"find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction" "find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
>; >;
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"find" | "findBySecretKeys" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
>;
secretVersionV2BridgeDAL: Pick<
TSecretVersionV2DALFactory,
"find" | "insertMany" | "update" | "findLatestVersionMany"
>;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById" | "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById" | "findByFolderIds">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
"findSecretPathByFolderIds" | "findBySecretPath" | "create" | "findOne" | "findByManySecretPath" "findSecretPathByFolderIds" | "findBySecretPath" | "create" | "findOne" | "findByManySecretPath"
>; >;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">; queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">; secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">; secretTagDAL: Pick<
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "find">; TSecretTagDALFactory,
| "findManyTagsById"
| "saveTagsToSecret"
| "deleteTagsManySecret"
| "find"
| "saveTagsToSecretV2"
| "deleteTagsToSecretV2"
>;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">; secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
secretApprovalRequestSecretDAL: Pick< secretApprovalRequestSecretDAL: Pick<
TSecretApprovalRequestSecretDALFactory, TSecretApprovalRequestSecretDALFactory,
"insertMany" | "insertApprovalSecretTags" "insertMany" | "insertApprovalSecretTags" | "insertV2Bridge"
>; >;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>; export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>;
@@ -90,9 +118,13 @@ export const secretReplicationServiceFactory = ({
secretApprovalRequestSecretDAL, secretApprovalRequestSecretDAL,
secretApprovalRequestDAL, secretApprovalRequestDAL,
secretQueueService, secretQueueService,
projectBotService projectBotService,
secretVersionV2TagBridgeDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService
}: TSecretReplicationServiceFactoryDep) => { }: TSecretReplicationServiceFactoryDep) => {
const getReplicatedSecrets = ( const $getReplicatedSecrets = (
botKey: string, botKey: string,
localSecrets: TSecrets[], localSecrets: TSecrets[],
importedSecrets: { secrets: TSecrets[] }[] importedSecrets: { secrets: TSecrets[] }[]
@@ -119,6 +151,25 @@ export const secretReplicationServiceFactory = ({
return secrets; return secrets;
}; };
const $getReplicatedSecretsV2 = (
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[],
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[]
) => {
const deDupe = new Set<string>();
const secrets = [...localSecrets];
for (let i = importedSecrets.length - 1; i >= 0; i = -1) {
importedSecrets[i].secrets.forEach((el) => {
if (deDupe.has(el.key)) {
return;
}
deDupe.add(el.key);
secrets.push(el);
});
}
return secrets;
};
// IMPORTANT NOTE BEFORE READING THE FUNCTION // IMPORTANT NOTE BEFORE READING THE FUNCTION
// SOURCE - Where secrets are copied from // SOURCE - Where secrets are copied from
// DESTINATION - Where the replicated imports that points to SOURCE from Destination // DESTINATION - Where the replicated imports that points to SOURCE from Destination
@@ -139,6 +190,7 @@ export const secretReplicationServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
if (!folder) return; if (!folder) return;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
// the the replicated imports made to the source. These are the destinations // the the replicated imports made to the source. These are the destinations
const destinationSecretImports = await secretImportDAL.find({ const destinationSecretImports = await secretImportDAL.find({
@@ -191,8 +243,270 @@ export const secretReplicationServiceFactory = ({
: destinationReplicatedSecretImports; : destinationReplicatedSecretImports;
if (!destinationReplicatedSecretImports.length) return; if (!destinationReplicatedSecretImports.length) return;
const botKey = await projectBotService.getBotKey(projectId); if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
// these are the secrets to be added in replicated folders
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
const sourceImportedSecrets = await fnSecretsV2FromImports({
allowedImports: sourceSecretImports,
secretDAL: secretV2BridgeDAL,
folderDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
});
// secrets that gets replicated across imports
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
...el,
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined
}));
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
const lock = await keyStore.acquireLock(
[getReplicationKeyLockPrefix(projectId, environmentSlug, secretPath)],
5000
);
try {
/* eslint-disable no-await-in-loop */
for (const destinationSecretImport of destinationReplicatedSecretImports) {
try {
const hasJobCompleted = await keyStore.getItem(
keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
KeyStorePrefixes.SecretReplication
);
if (hasJobCompleted) {
logger.info(
{ jobId: job.id, importId: destinationSecretImport.id },
"Skipping this job as this has been successfully replicated."
);
// eslint-disable-next-line
continue;
}
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
destinationSecretImport.folderId
]);
if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" });
let destinationReplicationFolder = await folderDAL.findOne({
parentId: destinationFolder.id,
name: getReplicationFolderName(destinationSecretImport.id),
isReserved: true
});
if (!destinationReplicationFolder) {
destinationReplicationFolder = await folderDAL.create({
parentId: destinationFolder.id,
name: getReplicationFolderName(destinationSecretImport.id),
envId: destinationFolder.envId,
isReserved: true
});
}
const destinationReplicationFolderId = destinationReplicationFolder.id;
const destinationLocalSecretsFromDB = await secretV2BridgeDAL.find({
folderId: destinationReplicationFolderId
});
const destinationLocalSecrets = destinationLocalSecretsFromDB.map((el) => ({
...el,
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined
}));
const destinationLocalSecretsGroupedByKey = groupBy(destinationLocalSecrets, (i) => i.key);
const locallyCreatedSecrets = sourceSecrets
.filter(({ key }) => !destinationLocalSecretsGroupedByKey[key]?.[0])
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
const locallyUpdatedSecrets = sourceSecrets
.filter(
({ key, secretKey, secretValue }) =>
destinationLocalSecretsGroupedByKey[key]?.[0] &&
// if key or value changed
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue)
)
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
const locallyDeletedSecrets = destinationLocalSecrets
.filter(({ key }) => !sourceSecretsGroupByKey[key]?.[0])
.map((el) => ({ ...el, operation: SecretOperations.Delete }));
const isEmtpy =
locallyCreatedSecrets.length + locallyUpdatedSecrets.length + locallyDeletedSecrets.length === 0;
// eslint-disable-next-line
if (isEmtpy) continue;
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
projectId,
destinationFolder.environmentSlug,
destinationFolder.path
);
// this means it should be a approval request rather than direct replication
if (policy && actor === ActorType.USER) {
const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(
destinationReplicationFolderId,
localSecretsLatestVersions
);
await secretApprovalRequestDAL.transaction(async (tx) => {
const approvalRequestDoc = await secretApprovalRequestDAL.create(
{
folderId: destinationReplicationFolderId,
slug: alphaNumericNanoId(),
policyId: policy.id,
status: "open",
hasMerged: false,
committerUserId: actorId,
isReplicated: true
},
tx
);
const commits = locallyCreatedSecrets
.concat(locallyUpdatedSecrets)
.concat(locallyDeletedSecrets)
.map((doc) => {
const { operation } = doc;
const localSecret = destinationLocalSecretsGroupedByKey[doc.key]?.[0];
return {
op: operation,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
key: doc.key,
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
// except create operation other two needs the secret id and version id
...(operation !== SecretOperations.Create
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
: {})
};
});
const approvalCommits = await secretApprovalRequestSecretDAL.insertV2Bridge(commits, tx);
return { ...approvalRequestDoc, commits: approvalCommits };
});
} else {
await secretDAL.transaction(async (tx) => {
if (locallyCreatedSecrets.length) {
await fnSecretV2BridgeBulkInsert({
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
secretTagDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
type: doc.type,
metadata: doc.metadata,
key: doc.key,
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
};
})
});
}
if (locallyUpdatedSecrets.length) {
await fnSecretV2BridgeBulkUpdate({
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
secretTagDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyUpdatedSecrets.map((doc) => {
return {
filter: {
folderId: destinationReplicationFolderId,
id: destinationLocalSecretsGroupedByKey[doc.key][0].id
},
data: {
type: doc.type,
metadata: doc.metadata,
key: doc.key,
encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
}
};
})
});
}
if (locallyDeletedSecrets.length) {
await secretDAL.delete(
{
$in: {
id: locallyDeletedSecrets.map(({ id }) => id)
},
folderId: destinationReplicationFolderId
},
tx
);
}
});
await secretQueueService.syncSecrets({
projectId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,
actor,
_depth: depth + 1,
_deDupeReplicationQueue: deDupeReplicationQueue,
_deDupeQueue: deDupeQueue
});
}
// this is used to avoid multiple times generating secret approval by failed one
await keyStore.setItemWithExpiry(
keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
SECRET_IMPORT_SUCCESS_LOCK,
1,
KeyStorePrefixes.SecretReplication
);
await secretImportDAL.updateById(destinationSecretImport.id, {
lastReplicated: new Date(),
replicationStatus: null,
isReplicationSuccess: true
});
} catch (err) {
logger.error(
err,
`Failed to replicate secret with import id=[${destinationSecretImport.id}] env=[${destinationSecretImport.importEnv.slug}] path=[${destinationSecretImport.importPath}]`
);
await secretImportDAL.updateById(destinationSecretImport.id, {
lastReplicated: new Date(),
replicationStatus: (err as Error)?.message.slice(0, 500),
isReplicationSuccess: false
});
}
}
/* eslint-enable no-await-in-loop */
} finally {
await lock.release();
logger.info(job.data, "Replication finished");
}
return;
}
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
// these are the secrets to be added in replicated folders // these are the secrets to be added in replicated folders
const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared }); const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id }); const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
@@ -203,7 +517,7 @@ export const secretReplicationServiceFactory = ({
secretImportDAL secretImportDAL
}); });
// secrets that gets replicated across imports // secrets that gets replicated across imports
const sourceSecrets = getReplicatedSecrets(botKey, sourceLocalSecrets, sourceImportedSecrets); const sourceSecrets = $getReplicatedSecrets(botKey, sourceLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string); const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string);
const lock = await keyStore.acquireLock( const lock = await keyStore.acquireLock(
@@ -372,7 +686,8 @@ export const secretReplicationServiceFactory = ({
secretCommentIV: doc.secretCommentIV, secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag, secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext, secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue)
}; };
}) })
}); });
@@ -407,7 +722,8 @@ export const secretReplicationServiceFactory = ({
secretCommentIV: doc.secretCommentIV, secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag, secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext, secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding skipMultilineEncoding: doc.skipMultilineEncoding,
references: getAllNestedSecretReferences(doc.secretValue)
} }
}; };
}) })
@@ -10,6 +10,7 @@ export type TSecretRotationDALFactory = ReturnType<typeof secretRotationDALFacto
export const secretRotationDALFactory = (db: TDbClient) => { export const secretRotationDALFactory = (db: TDbClient) => {
const secretRotationOrm = ormify(db, TableName.SecretRotation); const secretRotationOrm = ormify(db, TableName.SecretRotation);
const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput); const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput);
const secretRotationOutputV2Orm = ormify(db, TableName.SecretRotationOutputV2);
const findQuery = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) => const findQuery = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
tx(TableName.SecretRotation) tx(TableName.SecretRotation)
@@ -31,13 +32,7 @@ export const secretRotationDALFactory = (db: TDbClient) => {
.select(tx.ref("version").withSchema(TableName.Secret).as("secVersion")) .select(tx.ref("version").withSchema(TableName.Secret).as("secVersion"))
.select(tx.ref("secretKeyIV").withSchema(TableName.Secret)) .select(tx.ref("secretKeyIV").withSchema(TableName.Secret))
.select(tx.ref("secretKeyTag").withSchema(TableName.Secret)) .select(tx.ref("secretKeyTag").withSchema(TableName.Secret))
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret)) .select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret));
.select(tx.ref("secretValueIV").withSchema(TableName.Secret))
.select(tx.ref("secretValueTag").withSchema(TableName.Secret))
.select(tx.ref("secretValueCiphertext").withSchema(TableName.Secret))
.select(tx.ref("secretCommentIV").withSchema(TableName.Secret))
.select(tx.ref("secretCommentTag").withSchema(TableName.Secret))
.select(tx.ref("secretCommentCiphertext").withSchema(TableName.Secret));
const find = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => { const find = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
try { try {
@@ -54,33 +49,65 @@ export const secretRotationDALFactory = (db: TDbClient) => {
{ {
key: "secId", key: "secId",
label: "outputs" as const, label: "outputs" as const,
mapper: ({ mapper: ({ secId, outputKey, secVersion, secretKeyIV, secretKeyTag, secretKeyCiphertext }) => ({
secId,
outputKey,
secVersion,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretValueTag,
secretValueIV,
secretValueCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext
}) => ({
key: outputKey, key: outputKey,
secret: { secret: {
id: secId, id: secId,
version: secVersion, version: secVersion,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretKeyCiphertext, secretKeyCiphertext
secretValueTag, }
secretValueIV, })
secretValueCiphertext, }
secretCommentIV, ]
secretCommentTag, });
secretCommentCiphertext } catch (error) {
throw new DatabaseError({ error, name: "SecretRotationFind" });
}
};
const findQuerySecretV2 = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
tx(TableName.SecretRotation)
.where(filter)
.join(TableName.Environment, `${TableName.SecretRotation}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.SecretRotationOutputV2,
`${TableName.SecretRotation}.id`,
`${TableName.SecretRotationOutputV2}.rotationId`
)
.join(TableName.SecretV2, `${TableName.SecretRotationOutputV2}.secretId`, `${TableName.SecretV2}.id`)
.select(selectAllTableCols(TableName.SecretRotation))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(tx.ref("key").withSchema(TableName.SecretRotationOutputV2).as("outputKey"))
.select(tx.ref("id").withSchema(TableName.SecretV2).as("secId"))
.select(tx.ref("version").withSchema(TableName.SecretV2).as("secVersion"))
.select(tx.ref("key").withSchema(TableName.SecretV2).as("secretKey"));
const findSecretV2 = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
try {
const data = await findQuerySecretV2(filter, tx || db.replicaNode());
return sqlNestRelationships({
data,
key: "id",
parentMapper: (el) => ({
...SecretRotationsSchema.parse(el),
projectId: el.projectId,
environment: { id: el.envId, name: el.envName, slug: el.envSlug }
}),
childrenMapper: [
{
key: "secId",
label: "outputs" as const,
mapper: ({ secId, outputKey, secVersion, secretKey }) => ({
key: outputKey,
secret: {
id: secId,
version: secVersion,
secretKey
} }
}) })
} }
@@ -114,12 +141,19 @@ export const secretRotationDALFactory = (db: TDbClient) => {
}; };
const findRotationOutputsByRotationId = async (rotationId: string) => secretRotationOutputOrm.find({ rotationId }); const findRotationOutputsByRotationId = async (rotationId: string) => secretRotationOutputOrm.find({ rotationId });
const findRotationOutputsV2ByRotationId = async (rotationId: string) =>
secretRotationOutputV2Orm.find({ rotationId });
// special query
return { return {
...secretRotationOrm, ...secretRotationOrm,
find, find,
findSecretV2,
findById, findById,
secretOutputInsertMany: secretRotationOutputOrm.insertMany, secretOutputInsertMany: secretRotationOutputOrm.insertMany,
findRotationOutputsByRotationId secretOutputV2InsertMany: secretRotationOutputV2Orm.insertMany,
findRotationOutputsByRotationId,
findRotationOutputsV2ByRotationId
}; };
}; };
@@ -17,9 +17,13 @@ import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -47,8 +51,11 @@ type TSecretRotationQueueFactoryDep = {
secretRotationDAL: TSecretRotationDALFactory; secretRotationDAL: TSecretRotationDALFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">; secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "find">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">; telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
// These error should stop the repeatable job and ask user to reconfigure rotation // These error should stop the repeatable job and ask user to reconfigure rotation
@@ -70,7 +77,10 @@ export const secretRotationQueueFactory = ({
projectBotService, projectBotService,
secretDAL, secretDAL,
secretVersionDAL, secretVersionDAL,
telemetryService telemetryService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
kmsService
}: TSecretRotationQueueFactoryDep) => { }: TSecretRotationQueueFactoryDep) => {
const addToQueue = async (rotationId: string, interval: number) => { const addToQueue = async (rotationId: string, interval: number) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -111,7 +121,13 @@ export const secretRotationQueueFactory = ({
try { try {
if (!rotationProvider || !secretRotation) throw new DisableRotationErrors({ message: "Provider not found" }); if (!rotationProvider || !secretRotation) throw new DisableRotationErrors({ message: "Provider not found" });
const rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(secretRotation.projectId);
let rotationOutputs;
if (shouldUseSecretV2Bridge) {
rotationOutputs = await secretRotationDAL.findRotationOutputsV2ByRotationId(rotationId);
} else {
rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
}
if (!rotationOutputs.length) throw new DisableRotationErrors({ message: "Secrets not found" }); if (!rotationOutputs.length) throw new DisableRotationErrors({ message: "Secrets not found" });
// deep copy // deep copy
@@ -267,62 +283,112 @@ export const secretRotationQueueFactory = ({
internal: newCredential.internal internal: newCredential.internal
}); });
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables)); const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables));
const key = await projectBotService.getBotKey(secretRotation.projectId); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({ type: KmsDataKey.SecretManager,
secretId, projectId: secretRotation.projectId
value: encryptSymmetric128BitHexKeyUTF8(
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey]),
key
)
}));
// map the final values to output keys in the board
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
},
tx
);
const updatedSecrets = await secretDAL.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({
// this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag
}
})),
tx
);
await secretVersionDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" });
return {
...el,
secretId: id,
secretBlindIndex: el.secretBlindIndex
};
}),
tx
);
}); });
const numberOfSecretsRotated = rotationOutputs.length;
if (shouldUseSecretV2Bridge) {
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
secretId,
value:
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey])
}));
// map the final values to output keys in the board
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
},
tx
);
const updatedSecrets = await secretV2BridgeDAL.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({
// this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
}
})),
tx
);
await secretVersionV2BridgeDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el,
secretId: id
})),
tx
);
});
} else {
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
secretId,
value: encryptSymmetric128BitHexKeyUTF8(
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey]),
botKey
)
}));
// map the final values to output keys in the board
await secretRotationDAL.transaction(async (tx) => {
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
},
tx
);
const updatedSecrets = await secretDAL.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({
// this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag
}
})),
tx
);
await secretVersionDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" });
return {
...el,
secretId: id,
secretBlindIndex: el.secretBlindIndex
};
}),
tx
);
});
}
await telemetryService.sendPostHogEvents({ await telemetryService.sendPostHogEvents({
event: PostHogEventTypes.SecretRotated, event: PostHogEventTypes.SecretRotated,
distinctId: "", distinctId: "",
properties: { properties: {
numberOfSecrets: encryptedSecrets.length, numberOfSecrets: numberOfSecretsRotated,
environment: secretRotation.environment.slug, environment: secretRotation.environment.slug,
secretPath: secretRotation.secretPath, secretPath: secretRotation.secretPath,
workspaceId: secretRotation.projectId workspaceId: secretRotation.projectId
@@ -1,12 +1,15 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv"; import Ajv from "ajv";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { ProjectVersion } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/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 { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -22,9 +25,11 @@ type TSecretRotationServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
secretDAL: Pick<TSecretDALFactory, "find">; secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory; secretRotationQueue: TSecretRotationQueueFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
}; };
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>; export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
@@ -37,7 +42,9 @@ export const secretRotationServiceFactory = ({
licenseService, licenseService,
projectDAL, projectDAL,
folderDAL, folderDAL,
secretDAL secretDAL,
projectBotService,
secretV2BridgeDAL
}: TSecretRotationServiceFactoryDep) => { }: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({ const getProviderTemplates = async ({
actor, actor,
@@ -92,15 +99,25 @@ export const secretRotationServiceFactory = ({
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
const shouldUseBridge = project.version === ProjectVersion.V3;
if (shouldUseBridge) {
const selectedSecrets = await secretV2BridgeDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
} else {
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
}
const plan = await licenseService.getPlan(project.orgId); const plan = await licenseService.getPlan(project.orgId);
if (!plan.secretRotation) if (!plan.secretRotation)
throw new BadRequestError({ throw new BadRequestError({
@@ -148,10 +165,18 @@ export const secretRotationServiceFactory = ({
}, },
tx tx
); );
const outputSecretMapping = await secretRotationDAL.secretOutputInsertMany( let outputSecretMapping;
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })), if (shouldUseBridge) {
tx outputSecretMapping = await secretRotationDAL.secretOutputV2InsertMany(
); Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
} else {
outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
}
return { ...doc, outputs: outputSecretMapping, environment: folder.environment }; return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
}); });
await secretRotationQueue.addToQueue(secretRotation.id, secretRotation.interval); await secretRotationQueue.addToQueue(secretRotation.id, secretRotation.interval);
@@ -167,8 +192,30 @@ export const secretRotationServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
const doc = await secretRotationDAL.find({ projectId }); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
return doc; if (shouldUseSecretV2Bridge) {
const docs = await secretRotationDAL.findSecretV2({ projectId });
return docs;
}
if (!botKey) throw new BadRequestError({ message: "bot not found" });
const docs = await secretRotationDAL.find({ projectId });
return docs.map((el) => ({
...el,
outputs: el.outputs.map((output) => ({
...output,
secret: {
id: output.secret.id,
version: output.secret.version,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: output.secret.secretKeyCiphertext,
iv: output.secret.secretKeyIV,
tag: output.secret.secretKeyTag,
key: botKey
})
}
}))
}));
}; };
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => { const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {
@@ -1,15 +1,22 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert } from "@app/db/schemas"; import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -23,20 +30,27 @@ import {
import { TSnapshotDALFactory } from "./snapshot-dal"; import { TSnapshotDALFactory } from "./snapshot-dal";
import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal"; import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal"; import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
import { TSnapshotSecretV2DALFactory } from "./snapshot-secret-v2-dal";
import { getFullFolderPath } from "./snapshot-service-fns"; import { getFullFolderPath } from "./snapshot-service-fns";
type TSecretSnapshotServiceFactoryDep = { type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory; snapshotDAL: TSnapshotDALFactory;
snapshotSecretDAL: TSnapshotSecretDALFactory; snapshotSecretDAL: TSnapshotSecretDALFactory;
snapshotSecretV2BridgeDAL: TSnapshotSecretV2DALFactory;
snapshotFolderDAL: TSnapshotFolderDALFactory; snapshotFolderDAL: TSnapshotFolderDALFactory;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionByFolderId">; secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionByFolderId">;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionByFolderId">;
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestVersionByFolderId" | "insertMany">; folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestVersionByFolderId" | "insertMany">;
secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">; secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">; secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "delete" | "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "saveTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany" | "find">; folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany" | "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">; licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
}; };
export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>; export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>;
@@ -52,7 +66,13 @@ export const secretSnapshotServiceFactory = ({
permissionService, permissionService,
licenseService, licenseService,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
snapshotSecretV2BridgeDAL,
secretVersionV2TagBridgeDAL,
kmsService,
projectBotService
}: TSecretSnapshotServiceFactoryDep) => { }: TSecretSnapshotServiceFactoryDep) => {
const projectSecretSnapshotCount = async ({ const projectSecretSnapshotCount = async ({
environment, environment,
@@ -118,7 +138,7 @@ export const secretSnapshotServiceFactory = ({
}; };
const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => { const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
const snapshot = await snapshotDAL.findSecretSnapshotDataById(id); const snapshot = await snapshotDAL.findById(id);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -127,31 +147,122 @@ export const secretSnapshotServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3;
let snapshotDetails;
if (shouldUseBridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: snapshot.projectId
});
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
...el,
secretKey: el.key,
secretValue: el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined
}))
};
} else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
if (!botKey) throw new BadRequestError({ message: "bot not found" });
snapshotDetails = {
...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({
...el,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext,
iv: el.secretKeyIV,
tag: el.secretKeyTag,
key: botKey
}),
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
}),
secretComment:
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
tag: el.secretCommentTag,
key: botKey
})
: ""
}))
};
}
const fullFolderPath = await getFullFolderPath({ const fullFolderPath = await getFullFolderPath({
folderDAL, folderDAL,
folderId: snapshot.folderId, folderId: snapshotDetails.folderId,
envId: snapshot.environment.id envId: snapshotDetails.environment.id
}); });
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: snapshot.environment.slug, secretPath: fullFolderPath }) subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath
})
); );
return snapshot; return snapshotDetails;
}; };
const performSnapshot = async (folderId: string) => { const performSnapshot = async (folderId: string) => {
try { try {
if (!licenseService.isValidLicense) throw new InternalServerError({ message: "Invalid license" }); if (!licenseService.isValidLicense) throw new InternalServerError({ message: "Invalid license" });
const folder = await folderDAL.findById(folderId);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const shouldUseSecretV2Bridge = folder.projectVersion === 3;
if (shouldUseSecretV2Bridge) {
const snapshot = await snapshotDAL.transaction(async (tx) => {
const secretVersions = await secretVersionV2BridgeDAL.findLatestVersionByFolderId(folderId, tx);
const folderVersions = await folderVersionDAL.findLatestVersionByFolderId(folderId, tx);
const newSnapshot = await snapshotDAL.create(
{
folderId,
envId: folder.environment.envId,
parentFolderId: folder.parentId
},
tx
);
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
secretVersions.map(({ id }) => ({
secretVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);
const snapshotFolders = await snapshotFolderDAL.insertMany(
folderVersions.map(({ id }) => ({
folderVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);
return { ...newSnapshot, secrets: snapshotSecrets, folder: snapshotFolders };
});
return snapshot;
}
const snapshot = await snapshotDAL.transaction(async (tx) => { const snapshot = await snapshotDAL.transaction(async (tx) => {
const folder = await folderDAL.findById(folderId, tx);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const secretVersions = await secretVersionDAL.findLatestVersionByFolderId(folderId, tx); const secretVersions = await secretVersionDAL.findLatestVersionByFolderId(folderId, tx);
const folderVersions = await folderVersionDAL.findLatestVersionByFolderId(folderId, tx); const folderVersions = await folderVersionDAL.findLatestVersionByFolderId(folderId, tx);
const newSnapshot = await snapshotDAL.create( const newSnapshot = await snapshotDAL.create(
@@ -199,6 +310,7 @@ export const secretSnapshotServiceFactory = ({
}: TRollbackSnapshotDTO) => { }: TRollbackSnapshotDTO) => {
const snapshot = await snapshotDAL.findById(snapshotId); const snapshot = await snapshotDAL.findById(snapshotId);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" }); if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
const shouldUseBridge = snapshot.projectVersion === 3;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -212,6 +324,117 @@ export const secretSnapshotServiceFactory = ({
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
if (shouldUseBridge) {
const rollback = await snapshotDAL.transaction(async (tx) => {
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshotsV2Bridge(snapshot.id, tx);
// this will remove all secrets in current folder
const deletedTopLevelSecs = await secretV2BridgeDAL.delete({ folderId: snapshot.folderId }, tx);
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
// this will remove all secrets and folders on child
// due to sql foreign key and link list connection removing the folders removes everything below too
const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId, isReserved: false }, tx);
const deletedTopLevelFolders = groupBy(
deletedFolders.filter(({ parentId }) => parentId === snapshot.folderId),
(item) => item.id
);
const folders = await folderDAL.insertMany(
rollbackSnaps.flatMap(({ folderVersion, folderId }) =>
folderVersion.map(({ name, id, latestFolderVersion }) => ({
envId: snapshot.envId,
id,
// this means don't bump up the version if not root folder
// because below ones can be same version as nothing changed
version: deletedTopLevelFolders[folderId] ? latestFolderVersion + 1 : latestFolderVersion,
name,
parentId: folderId
}))
),
tx
);
const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map(
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
...el,
id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
folderId
})
)
),
tx
);
const secretTagsToBeInsert: TSecretV2TagJunctionInsert[] = [];
const secretVerTagToBeInsert: Record<string, string[]> = {};
rollbackSnaps.forEach(({ secretVersions }) => {
secretVersions.forEach((secVer) => {
secVer.tags.forEach((tag) => {
secretTagsToBeInsert.push({ secrets_v2Id: secVer.secretId, secret_tagsId: tag.id });
if (!secretVerTagToBeInsert?.[secVer.secretId]) secretVerTagToBeInsert[secVer.secretId] = [];
secretVerTagToBeInsert[secVer.secretId].push(tag.id);
});
});
});
await secretTagDAL.saveTagsToSecretV2(secretTagsToBeInsert, tx);
const folderVersions = await folderVersionDAL.insertMany(
folders.map(({ version, name, id, envId }) => ({
name,
version,
folderId: id,
envId
})),
tx
);
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })),
tx
);
await secretVersionV2TagBridgeDAL.insertMany(
secretVersions.flatMap(({ secretId, id }) =>
secretVerTagToBeInsert?.[secretId]?.length
? secretVerTagToBeInsert[secretId].map((tagId) => ({
[`${TableName.SecretTag}Id` as const]: tagId,
[`${TableName.SecretVersionV2}Id` as const]: id
}))
: []
),
tx
);
const newSnapshot = await snapshotDAL.create(
{
folderId: snapshot.folderId,
envId: snapshot.envId,
parentFolderId: snapshot.parentFolderId
},
tx
);
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
secretVersions
.filter(({ secretId }) => Boolean(deletedTopLevelSecsGroupById?.[secretId]))
.map(({ id }) => ({
secretVersionId: id,
envId: newSnapshot.envId,
snapshotId: newSnapshot.id
})),
tx
);
const snapshotFolders = await snapshotFolderDAL.insertMany(
folderVersions
.filter(({ folderId }) => Boolean(deletedTopLevelFolders?.[folderId]))
.map(({ id }) => ({
folderVersionId: id,
envId: newSnapshot.envId,
snapshotId: newSnapshot.id
})),
tx
);
return { ...newSnapshot, snapshotSecrets, snapshotFolders };
});
return rollback;
}
const rollback = await snapshotDAL.transaction(async (tx) => { const rollback = await snapshotDAL.transaction(async (tx) => {
const rollbackSnaps = await snapshotDAL.findRecursivelySnapshots(snapshot.id, tx); const rollbackSnaps = await snapshotDAL.findRecursivelySnapshots(snapshot.id, tx);
// this will remove all secrets in current folder // this will remove all secrets in current folder
@@ -1,14 +1,17 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { Knex } from "knex"; import { Knex } from "knex";
import { z } from "zod";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { import {
SecretVersionsSchema, SecretVersionsSchema,
SecretVersionsV2Schema,
TableName, TableName,
TSecretFolderVersions, TSecretFolderVersions,
TSecretSnapshotFolders, TSecretSnapshotFolders,
TSecretSnapshots, TSecretSnapshots,
TSecretVersions TSecretVersions,
TSecretVersionsV2
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@@ -24,12 +27,14 @@ export const snapshotDALFactory = (db: TDbClient) => {
const data = await (tx || db.replicaNode())(TableName.Snapshot) const data = await (tx || db.replicaNode())(TableName.Snapshot)
.where(`${TableName.Snapshot}.id`, id) .where(`${TableName.Snapshot}.id`, id)
.join(TableName.Environment, `${TableName.Snapshot}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.Snapshot}.envId`, `${TableName.Environment}.id`)
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.Snapshot)) .select(selectAllTableCols(TableName.Snapshot))
.select( .select(
db.ref("id").withSchema(TableName.Environment).as("envId"), db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("projectId").withSchema(TableName.Environment), db.ref("projectId").withSchema(TableName.Environment),
db.ref("name").withSchema(TableName.Environment).as("envName"), db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug") db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("version").withSchema(TableName.Project).as("projectVersion")
) )
.first(); .first();
if (data) { if (data) {
@@ -149,6 +154,101 @@ export const snapshotDALFactory = (db: TDbClient) => {
} }
}; };
const findSecretSnapshotV2DataById = async (snapshotId: string, tx?: Knex) => {
try {
const data = await (tx || db.replicaNode())(TableName.Snapshot)
.where(`${TableName.Snapshot}.id`, snapshotId)
.join(TableName.Environment, `${TableName.Snapshot}.envId`, `${TableName.Environment}.id`)
.leftJoin(TableName.SnapshotSecretV2, `${TableName.Snapshot}.id`, `${TableName.SnapshotSecretV2}.snapshotId`)
.leftJoin(
TableName.SecretVersionV2,
`${TableName.SnapshotSecretV2}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretVersionV2Tag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.SnapshotFolder, `${TableName.SnapshotFolder}.snapshotId`, `${TableName.Snapshot}.id`)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
db.ref("createdAt").withSchema(TableName.Snapshot).as("snapshotCreatedAt"),
db.ref("updatedAt").withSchema(TableName.Snapshot).as("snapshotUpdatedAt"),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("projectId").withSchema(TableName.Environment),
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId"),
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
);
return sqlNestRelationships({
data,
key: "snapshotId",
parentMapper: ({
snapshotId: id,
folderId,
projectId,
envId,
envSlug,
envName,
snapshotCreatedAt: createdAt,
snapshotUpdatedAt: updatedAt
}) => ({
id,
folderId,
projectId,
createdAt,
updatedAt,
environment: { id: envId, slug: envSlug, name: envName }
}),
childrenMapper: [
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => SecretVersionsV2Schema.parse(el),
childrenMapper: [
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
slug,
color,
vId
})
}
]
},
{
key: "folderVerId",
label: "folderVersion" as const,
mapper: ({ folderVerId: id, folderVerName: name }) => ({ id, name })
}
]
})?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindSecretSnapshotDataById" });
}
};
// this is used for rollback // this is used for rollback
// from a starting snapshot it will collect all the secrets and folder of that // from a starting snapshot it will collect all the secrets and folder of that
// then it will start go through recursively the below folders latest snapshots then their child folder snapshot until leaf node // then it will start go through recursively the below folders latest snapshots then their child folder snapshot until leaf node
@@ -304,6 +404,161 @@ export const snapshotDALFactory = (db: TDbClient) => {
} }
}; };
// this is used for rollback
// from a starting snapshot it will collect all the secrets and folder of that
// then it will start go through recursively the below folders latest snapshots then their child folder snapshot until leaf node
// the recursive part find all snapshot id
// then joins with respective secrets and folder
const findRecursivelySnapshotsV2Bridge = async (snapshotId: string, tx?: Knex) => {
try {
const data = await (tx || db)
.withRecursive("parent", (qb) => {
void qb
.from(TableName.Snapshot)
.leftJoin<TSecretSnapshotFolders>(
TableName.SnapshotFolder,
`${TableName.SnapshotFolder}.snapshotId`,
`${TableName.Snapshot}.id`
)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.select(selectAllTableCols(TableName.Snapshot))
.select({ depth: 1 })
.select(
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId")
)
.where(`${TableName.Snapshot}.id`, snapshotId)
.union(
(cb) =>
void cb
.select(selectAllTableCols(TableName.Snapshot))
.select({ depth: db.raw("parent.depth + 1") })
.select(
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderVerName"),
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderVerId")
)
.from(TableName.Snapshot)
.join<TSecretSnapshots, TSecretSnapshots & { secretId: string; max: number }>(
db(TableName.Snapshot).groupBy("folderId").max("createdAt").select("folderId").as("latestVersion"),
`${TableName.Snapshot}.createdAt`,
"latestVersion.max"
)
.leftJoin<TSecretSnapshotFolders>(
TableName.SnapshotFolder,
`${TableName.SnapshotFolder}.snapshotId`,
`${TableName.Snapshot}.id`
)
.leftJoin<TSecretFolderVersions>(
TableName.SecretFolderVersion,
`${TableName.SnapshotFolder}.folderVersionId`,
`${TableName.SecretFolderVersion}.id`
)
.join("parent", "parent.folderVerId", `${TableName.Snapshot}.folderId`)
);
})
.orderBy("depth", "asc")
.from<TSecretSnapshots & { folderVerId: string; folderVerName: string }>("parent")
.leftJoin<TSecretSnapshots>(TableName.SnapshotSecretV2, `parent.id`, `${TableName.SnapshotSecretV2}.snapshotId`)
.leftJoin<TSecretVersionsV2>(
TableName.SecretVersionV2,
`${TableName.SnapshotSecretV2}.secretVersionId`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretVersionV2Tag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`,
`${TableName.SecretVersionV2}.id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin<{ latestSecretVersion: number }>(
(tx || db)(TableName.SecretVersionV2)
.groupBy("secretId")
.select("secretId")
.max("version")
.as("secGroupByMaxVersion"),
`${TableName.SecretVersionV2}.secretId`,
"secGroupByMaxVersion.secretId"
)
.leftJoin<{ latestFolderVersion: number }>(
(tx || db)(TableName.SecretFolderVersion)
.groupBy("folderId")
.select("folderId")
.max("version")
.as("folderGroupByMaxVersion"),
`parent.folderId`,
"folderGroupByMaxVersion.folderId"
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(
db.ref("id").withSchema("parent").as("snapshotId"),
db.ref("folderId").withSchema("parent").as("snapshotFolderId"),
db.ref("parentFolderId").withSchema("parent").as("snapshotParentFolderId"),
db.ref("folderVerName").withSchema("parent"),
db.ref("folderVerId").withSchema("parent"),
db.ref("max").withSchema("secGroupByMaxVersion").as("latestSecretVersion"),
db.ref("max").withSchema("folderGroupByMaxVersion").as("latestFolderVersion"),
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
);
const formated = sqlNestRelationships({
data,
key: "snapshotId",
parentMapper: ({ snapshotId: id, snapshotFolderId: folderId, snapshotParentFolderId: parentFolderId }) => ({
id,
folderId,
parentFolderId
}),
childrenMapper: [
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => ({
...SecretVersionsV2Schema.parse(el),
latestSecretVersion: el.latestSecretVersion as number
}),
childrenMapper: [
{
key: "tagVersionId",
label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id,
name,
slug,
color,
vId
})
}
]
},
{
key: "folderVerId",
label: "folderVersion" as const,
mapper: ({ folderVerId: id, folderVerName: name, latestFolderVersion }) => ({
id,
name,
latestFolderVersion: latestFolderVersion as number
})
}
]
});
return formated;
} catch (error) {
throw new DatabaseError({ error, name: "FindRecursivelySnapshots" });
}
};
// instead of copying all child folders // instead of copying all child folders
// we will take the latest snapshot of those folders // we will take the latest snapshot of those folders
// when we need to rollback we will pull from these snapshots // when we need to rollback we will pull from these snapshots
@@ -465,13 +720,108 @@ export const snapshotDALFactory = (db: TDbClient) => {
} }
}; };
// special query for migration for secret v2
const findNSecretV1SnapshotByFolderId = async (folderId: string, n = 15, tx?: Knex) => {
try {
const query = (tx || db.replicaNode())(TableName.Snapshot)
.leftJoin(TableName.SnapshotSecret, `${TableName.Snapshot}.id`, `${TableName.SnapshotSecret}.snapshotId`)
.leftJoin(
TableName.SecretVersion,
`${TableName.SnapshotSecret}.secretVersionId`,
`${TableName.SecretVersion}.id`
)
.leftJoin(
TableName.SecretVersionTag,
`${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`,
`${TableName.SecretVersion}.id`
)
.select(selectAllTableCols(TableName.SecretVersion))
.select(
db.ref("id").withSchema(TableName.Snapshot).as("snapshotId"),
db.ref("createdAt").withSchema(TableName.Snapshot).as("snapshotCreatedAt"),
db.ref("updatedAt").withSchema(TableName.Snapshot).as("snapshotUpdatedAt"),
db.ref("envId").withSchema(TableName.SnapshotSecret).as("snapshotEnvId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("secretVersionTagId"),
db.ref("secret_versionsId").withSchema(TableName.SecretVersionTag).as("secretVersionTagSecretId"),
db.ref("secret_tagsId").withSchema(TableName.SecretVersionTag).as("secretVersionTagSecretTagId"),
db.raw(
`DENSE_RANK() OVER (partition by ${TableName.Snapshot}."id" ORDER BY ${TableName.SecretVersion}."createdAt") as rank`
)
)
.orderBy(`${TableName.Snapshot}.createdAt`, "desc")
.where(`${TableName.Snapshot}.folderId`, folderId);
const data = await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.andWhere("w.rank", "<", n);
return sqlNestRelationships({
data,
key: "snapshotId",
parentMapper: ({ snapshotId: id, snapshotCreatedAt: createdAt, snapshotUpdatedAt: updatedAt }) => ({
id,
folderId,
createdAt,
updatedAt
}),
childrenMapper: [
{
key: "id",
label: "secretVersions" as const,
mapper: (el) => SecretVersionsSchema.extend({ snapshotEnvId: z.string() }).parse(el),
childrenMapper: [
{
key: "secretVersionTagId",
label: "tags" as const,
mapper: ({ secretVersionTagId, secretVersionTagSecretId, secretVersionTagSecretTagId }) => ({
id: secretVersionTagId,
secretVersionId: secretVersionTagSecretId,
secretTagId: secretVersionTagSecretTagId
})
}
]
}
]
});
} catch (error) {
throw new DatabaseError({ error, name: "FindSecretSnapshotDataById" });
}
};
const deleteSnapshotsAboveLimit = async (folderId: string, n = 15, tx?: Knex) => {
try {
const query = await (tx || db)
.with("to_delete", (qb) => {
void qb
.select("id")
.from(TableName.Snapshot)
.where("folderId", folderId)
.orderBy("createdAt", "desc")
.offset(n);
})
.from(TableName.Snapshot)
.whereIn("id", (qb) => {
void qb.select("id").from("to_delete");
})
.delete();
return query;
} catch (error) {
throw new DatabaseError({ error, name: "DeleteSnapshotsAboveLimit" });
}
};
return { return {
...secretSnapshotOrm, ...secretSnapshotOrm,
findById, findById,
findLatestSnapshotByFolderId, findLatestSnapshotByFolderId,
findRecursivelySnapshots, findRecursivelySnapshots,
findRecursivelySnapshotsV2Bridge,
countOfSnapshotsByFolderId, countOfSnapshotsByFolderId,
findSecretSnapshotDataById, findSecretSnapshotDataById,
pruneExcessSnapshots findSecretSnapshotV2DataById,
pruneExcessSnapshots,
findNSecretV1SnapshotByFolderId,
deleteSnapshotsAboveLimit
}; };
}; };
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TSnapshotSecretV2DALFactory = ReturnType<typeof snapshotSecretV2DALFactory>;
export const snapshotSecretV2DALFactory = (db: TDbClient) => {
const snapshotSecretOrm = ormify(db, TableName.SnapshotSecretV2);
return snapshotSecretOrm;
};
+11 -3
View File
@@ -6,7 +6,15 @@ export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
// all the key prefixes used must be set here to avoid conflict // all the key prefixes used must be set here to avoid conflict
export enum KeyStorePrefixes { export enum KeyStorePrefixes {
SecretReplication = "secret-replication-import-lock" SecretReplication = "secret-replication-import-lock",
KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock",
KmsProjectKeyCreation = "kms-project-key-creation-lock",
WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-",
WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-",
KmsOrgKeyCreation = "kms-org-key-creation-lock",
KmsOrgDataKeyCreation = "kms-org-data-key-creation-lock",
WaitUntilReadyKmsOrgKeyCreation = "wait-until-ready-kms-org-key-creation-",
WaitUntilReadyKmsOrgDataKeyCreation = "wait-until-ready-kms-org-data-key-creation-"
} }
type TWaitTillReady = { type TWaitTillReady = {
@@ -32,7 +40,7 @@ export const keyStoreFactory = (redisUrl: string) => {
exp: number | string, exp: number | string,
value: string | number | Buffer, value: string | number | Buffer,
prefix?: string prefix?: string
) => redis.setex(prefix ? `${prefix}:${key}` : key, exp, value); ) => redis.set(prefix ? `${prefix}:${key}` : key, value, "EX", exp);
const deleteItem = async (key: string) => redis.del(key); const deleteItem = async (key: string) => redis.del(key);
@@ -57,7 +65,7 @@ export const keyStoreFactory = (redisUrl: string) => {
}); });
attempts += 1; attempts += 1;
// eslint-disable-next-line // eslint-disable-next-line
isReady = keyCheckCb(await getItem(key, "wait_till_ready")); isReady = keyCheckCb(await getItem(key));
} }
}; };
+7 -2
View File
@@ -608,7 +608,9 @@ 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 create.", type: "The type of the secret to create.",
workspaceId: "The ID of the project to create the secret in.", workspaceId: "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",
secretReminderNote: "Note to be attached in notification email"
}, },
GET: { GET: {
expand: "Whether or not to expand secret references", expand: "Whether or not to expand secret references",
@@ -631,7 +633,10 @@ export const RAW_SECRETS = {
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.", workspaceId: "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",
secretReminderNote: "Note to be attached in notification email",
newSecretName: "The new name for the secret"
}, },
DELETE: { DELETE: {
secretName: "The name of the secret to delete.", secretName: "The name of the secret to delete.",
+5 -2
View File
@@ -116,6 +116,8 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64"); export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
export const generateAsymmetricKeyPair = () => { export const generateAsymmetricKeyPair = () => {
const pair = nacl.box.keyPair(); const pair = nacl.box.keyPair();
@@ -224,8 +226,9 @@ export const infisicalSymmetricDecrypt = <T = string>({
keyEncoding: SecretKeyEncoding; keyEncoding: SecretKeyEncoding;
}) => { }) => {
const appCfg = getConfig(); const appCfg = getConfig();
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; // the or gate is used used in migration
const encryptionKey = appCfg.ENCRYPTION_KEY; const rootEncryptionKey = appCfg?.ROOT_ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY;
const encryptionKey = appCfg?.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) { if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext }); const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext });
return data as T; return data as T;
+17
View File
@@ -17,6 +17,23 @@ export const groupBy = <T, Key extends string | number | symbol>(
{} as Record<Key, T[]> {} as Record<Key, T[]>
); );
/**
* Sorts an array of items into groups. The return value is a map where the keys are
* the group ids the given getGroupId function produced and the value will be the last found one for the group key
*/
export const groupByUnique = <T, Key extends string | number | symbol>(
array: readonly T[],
getGroupId: (item: T) => Key
): Record<Key, T> =>
array.reduce(
(acc, item) => {
const groupId = getGroupId(item);
acc[groupId] = item;
return acc;
},
{} as Record<Key, T>
);
/** /**
* Given a list of items returns a new list with only * Given a list of items returns a new list with only
* unique items. Accepts an optional identity function * unique items. Accepts an optional identity function
+1
View File
@@ -6,3 +6,4 @@ export * from "./array";
export * from "./dates"; export * from "./dates";
export * from "./object"; export * from "./object";
export * from "./string"; export * from "./string";
export * from "./undefined";
+3
View File
@@ -0,0 +1,3 @@
export const executeIfDefined = <T, R>(func: (input: T) => R, input: T | undefined): R | undefined => {
return input === undefined ? undefined : func(input);
};
+13
View File
@@ -104,6 +104,19 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Create" }); throw new DatabaseError({ error, name: "Create" });
} }
}, },
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
try {
if (!data.length) return [];
const res = await (tx || db)(tableName)
.insert(data as never)
.onConflict(onConflictField as never)
.merge()
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Create" });
}
},
updateById: async ( updateById: async (
id: string, id: string,
{ {
+9
View File
@@ -12,3 +12,12 @@ export const stripUndefinedInWhere = <T extends object>(val: T): Exclude<T, unde
}); });
return copy as Exclude<T, undefined>; return copy as Exclude<T, undefined>;
}; };
// if its undefined its skipped in knex
// if its empty string its set as null
// else pass to the required one
export const setKnexStringValue = <T>(value: string | null | undefined, cb: (arg: string) => T) => {
if (typeof value === "undefined") return;
if (value === "" || value === null) return null;
return cb(value);
};
+9 -2
View File
@@ -25,7 +25,8 @@ export enum QueueName {
DynamicSecretRevocation = "dynamic-secret-revocation", DynamicSecretRevocation = "dynamic-secret-revocation",
CaCrlRotation = "ca-crl-rotation", CaCrlRotation = "ca-crl-rotation",
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration"
} }
export enum QueueJobs { export enum QueueJobs {
@@ -44,7 +45,8 @@ export enum QueueJobs {
DynamicSecretPruning = "dynamic-secret-pruning", DynamicSecretPruning = "dynamic-secret-pruning",
CaCrlRotation = "ca-crl-rotation-job", CaCrlRotation = "ca-crl-rotation-job",
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -136,6 +138,10 @@ export type TQueueJobTypes = {
name: QueueJobs.SecretSync; name: QueueJobs.SecretSync;
payload: TSyncSecretsDTO; payload: TSyncSecretsDTO;
}; };
[QueueName.ProjectV3Migration]: {
name: QueueJobs.ProjectV3Migration;
payload: { projectId: string };
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
@@ -210,6 +216,7 @@ export const queueServiceFactory = (redisUrl: string) => {
const job = await q.getJob(jobId); const job = await q.getJob(jobId);
if (!job) return true; if (!job) return true;
if (!job.repeatJobKey) return true; if (!job.repeatJobKey) return true;
await job.remove();
return q.removeRepeatableByKey(job.repeatJobKey); return q.removeRepeatableByKey(job.repeatJobKey);
}; };
+107 -41
View File
@@ -66,6 +66,7 @@ import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/s
import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal"; import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal"; import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
@@ -160,6 +161,10 @@ import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sha
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service"; import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service"; import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { secretV2BridgeServiceFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-service";
import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service"; import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSmtpService } from "@app/services/smtp/smtp-service"; import { TSmtpService } from "@app/services/smtp/smtp-service";
@@ -229,6 +234,10 @@ export const registerRoutes = async (
const secretVersionTagDAL = secretVersionTagDALFactory(db); const secretVersionTagDAL = secretVersionTagDALFactory(db);
const secretBlindIndexDAL = secretBlindIndexDALFactory(db); const secretBlindIndexDAL = secretBlindIndexDALFactory(db);
const secretV2BridgeDAL = secretV2BridgeDALFactory(db);
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
const integrationDAL = integrationDALFactory(db); const integrationDAL = integrationDALFactory(db);
const integrationAuthDAL = integrationAuthDALFactory(db); const integrationAuthDAL = integrationAuthDALFactory(db);
const webhookDAL = webhookDALFactory(db); const webhookDAL = webhookDALFactory(db);
@@ -277,6 +286,7 @@ export const registerRoutes = async (
const secretRotationDAL = secretRotationDALFactory(db); const secretRotationDAL = secretRotationDALFactory(db);
const snapshotDAL = snapshotDALFactory(db); const snapshotDAL = snapshotDALFactory(db);
const snapshotSecretDAL = snapshotSecretDALFactory(db); const snapshotSecretDAL = snapshotSecretDALFactory(db);
const snapshotSecretV2BridgeDAL = snapshotSecretV2DALFactory(db);
const snapshotFolderDAL = snapshotFolderDALFactory(db); const snapshotFolderDAL = snapshotFolderDALFactory(db);
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db); const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
@@ -316,7 +326,8 @@ export const registerRoutes = async (
kmsDAL, kmsDAL,
kmsService, kmsService,
permissionService, permissionService,
externalKmsDAL externalKmsDAL,
licenseService
}); });
const trustedIpService = trustedIpServiceFactory({ const trustedIpService = trustedIpServiceFactory({
@@ -609,10 +620,8 @@ export const registerRoutes = async (
permissionService, permissionService,
projectDAL, projectDAL,
projectQueue: projectQueueService, projectQueue: projectQueueService,
secretBlindIndexDAL,
identityProjectDAL, identityProjectDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
projectBotDAL,
projectKeyDAL, projectKeyDAL,
userDAL, userDAL,
projectEnvDAL, projectEnvDAL,
@@ -625,7 +634,8 @@ export const registerRoutes = async (
certificateDAL, certificateDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore keyStore,
kmsService
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -655,13 +665,20 @@ export const registerRoutes = async (
secretVersionDAL, secretVersionDAL,
folderVersionDAL, folderVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL,
projectBotService,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
snapshotSecretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL
}); });
const webhookService = webhookServiceFactory({ const webhookService = webhookServiceFactory({
permissionService, permissionService,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL projectDAL,
kmsService
}); });
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
@@ -678,8 +695,8 @@ export const registerRoutes = async (
integrationAuthDAL, integrationAuthDAL,
integrationDAL, integrationDAL,
permissionService, permissionService,
projectBotDAL, projectBotService,
projectBotService kmsService
}); });
const secretQueueService = secretQueueFactory({ const secretQueueService = secretQueueFactory({
queueService, queueService,
@@ -699,46 +716,51 @@ export const registerRoutes = async (
secretVersionDAL, secretVersionDAL,
secretBlindIndexDAL, secretBlindIndexDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL,
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
secretRotationDAL,
integrationAuthDAL,
snapshotDAL,
snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL
}); });
const secretImportService = secretImportServiceFactory({ const secretImportService = secretImportServiceFactory({
licenseService, licenseService,
projectBotService,
projectEnvDAL, projectEnvDAL,
folderDAL, folderDAL,
permissionService, permissionService,
secretImportDAL, secretImportDAL,
projectDAL, projectDAL,
secretDAL, secretDAL,
secretQueueService secretQueueService,
secretV2BridgeDAL,
kmsService
}); });
const secretBlindIndexService = secretBlindIndexServiceFactory({ const secretBlindIndexService = secretBlindIndexServiceFactory({
permissionService, permissionService,
secretDAL, secretDAL,
secretBlindIndexDAL secretBlindIndexDAL
}); });
const secretService = secretServiceFactory({
folderDAL,
secretVersionDAL,
secretVersionTagDAL,
secretBlindIndexDAL,
permissionService,
projectDAL,
secretDAL,
secretTagDAL,
snapshotService,
secretQueueService,
secretImportDAL,
projectEnvDAL,
projectBotService,
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL
});
const secretSharingService = secretSharingServiceFactory({ const secretV2BridgeService = secretV2BridgeServiceFactory({
folderDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretQueueService,
secretDAL: secretV2BridgeDAL,
permissionService, permissionService,
secretSharingDAL, secretVersionTagDAL: secretVersionTagV2BridgeDAL,
orgDAL secretTagDAL,
projectEnvDAL,
secretImportDAL,
secretApprovalRequestDAL,
secretApprovalPolicyService,
secretApprovalRequestSecretDAL,
kmsService,
snapshotService
}); });
const secretApprovalRequestService = secretApprovalRequestServiceFactory({ const secretApprovalRequestService = secretApprovalRequestServiceFactory({
@@ -756,9 +778,40 @@ export const registerRoutes = async (
snapshotService, snapshotService,
secretVersionTagDAL, secretVersionTagDAL,
secretQueueService, secretQueueService,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
smtpService, smtpService,
userDAL, projectEnvDAL,
projectEnvDAL userDAL
});
const secretService = secretServiceFactory({
folderDAL,
secretVersionDAL,
secretVersionTagDAL,
secretBlindIndexDAL,
permissionService,
projectDAL,
secretDAL,
secretTagDAL,
snapshotService,
secretQueueService,
secretImportDAL,
projectEnvDAL,
projectBotService,
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
secretV2BridgeService,
secretApprovalRequestService
});
const secretSharingService = secretSharingServiceFactory({
permissionService,
secretSharingDAL,
orgDAL
}); });
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
@@ -794,11 +847,14 @@ export const registerRoutes = async (
queueService, queueService,
folderDAL, folderDAL,
secretApprovalPolicyService, secretApprovalPolicyService,
secretBlindIndexDAL,
secretApprovalRequestDAL, secretApprovalRequestDAL,
secretApprovalRequestSecretDAL, secretApprovalRequestSecretDAL,
secretQueueService, secretQueueService,
projectBotService projectBotService,
kmsService,
secretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL
}); });
const secretRotationQueue = secretRotationQueueFactory({ const secretRotationQueue = secretRotationQueueFactory({
telemetryService, telemetryService,
@@ -806,7 +862,10 @@ export const registerRoutes = async (
queue: queueService, queue: queueService,
secretDAL, secretDAL,
secretVersionDAL, secretVersionDAL,
projectBotService projectBotService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService
}); });
const secretRotationService = secretRotationServiceFactory({ const secretRotationService = secretRotationServiceFactory({
@@ -816,7 +875,9 @@ export const registerRoutes = async (
projectDAL, projectDAL,
licenseService, licenseService,
secretDAL, secretDAL,
folderDAL folderDAL,
projectBotService,
secretV2BridgeDAL
}); });
const integrationService = integrationServiceFactory({ const integrationService = integrationServiceFactory({
@@ -927,7 +988,9 @@ export const registerRoutes = async (
queueService, queueService,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretDAL dynamicSecretDAL,
kmsService,
folderDAL
}); });
const dynamicSecretService = dynamicSecretServiceFactory({ const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL, projectDAL,
@@ -937,7 +1000,8 @@ export const registerRoutes = async (
dynamicSecretProviders, dynamicSecretProviders,
folderDAL, folderDAL,
permissionService, permissionService,
licenseService licenseService,
kmsService
}); });
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL, projectDAL,
@@ -947,7 +1011,8 @@ export const registerRoutes = async (
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
folderDAL, folderDAL,
licenseService licenseService,
kmsService
}); });
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL, auditLogDAL,
@@ -956,7 +1021,8 @@ export const registerRoutes = async (
secretFolderVersionDAL: folderVersionDAL, secretFolderVersionDAL: folderVersionDAL,
snapshotDAL, snapshotDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL
}); });
const oidcService = oidcConfigServiceFactory({ const oidcService = oidcConfigServiceFactory({
+25 -7
View File
@@ -5,6 +5,7 @@ import {
IdentityProjectAdditionalPrivilegeSchema, IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema, IntegrationAuthsSchema,
ProjectRolesSchema, ProjectRolesSchema,
ProjectsSchema,
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
@@ -62,8 +63,14 @@ export const secretRawSchema = z.object({
version: z.number(), version: z.number(),
type: z.string(), type: z.string(),
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string(), secretValue: z.string().optional(),
secretComment: z.string().optional() secretComment: z.string().optional(),
secretReminderNote: z.string().nullable().optional(),
secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
metadata: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
}); });
export const ProjectPermissionSchema = z.object({ export const ProjectPermissionSchema = z.object({
@@ -122,11 +129,7 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({
}); });
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true, encryptedConfig: true
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
}); });
export const SanitizedAuditLogStreamSchema = z.object({ export const SanitizedAuditLogStreamSchema = z.object({
@@ -135,3 +138,18 @@ export const SanitizedAuditLogStreamSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()
}); });
export const SanitizedProjectSchema = ProjectsSchema.pick({
id: true,
name: true,
slug: true,
autoCapitalization: true,
orgId: true,
createdAt: true,
updatedAt: true,
version: true,
upgradeStatus: true,
pitVersionLimit: true,
kmsCertificateKeyId: true,
auditLogsRetentionDays: true
});
@@ -1,12 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
IdentitiesSchema,
IdentityOrgMembershipsSchema,
OrgMembershipRole,
OrgRolesSchema,
ProjectsSchema
} 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 { IDENTITIES } from "@app/lib/api-docs"; import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -15,6 +9,8 @@ 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"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerIdentityRouter = async (server: FastifyZodProvider) => { export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
@@ -307,7 +303,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
) )
}) })
+10 -16
View File
@@ -1,22 +1,16 @@
import { z } from "zod"; import { z } from "zod";
import { import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
IntegrationsSchema,
ProjectMembershipsSchema,
ProjectsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
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";
import { ProjectFilterType } from "@app/services/project/project-types"; import { ProjectFilterType } from "@app/services/project/project-types";
import { integrationAuthPubSchema } from "../sanitizedSchemas"; import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
import { sanitizedServiceTokenSchema } from "../v2/service-token-router"; import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
const projectWithEnv = ProjectsSchema.merge( const projectWithEnv = SanitizedProjectSchema.merge(
z.object({ z.object({
_id: z.string(), _id: z.string(),
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
@@ -78,7 +72,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
lastName: true, lastName: true,
id: true id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
project: ProjectsSchema.pick({ name: true, id: true }), project: SanitizedProjectSchema.pick({ name: true, id: true }),
roles: z.array( roles: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
@@ -188,7 +182,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspace: ProjectsSchema.optional() workspace: SanitizedProjectSchema.optional()
}) })
} }
}, },
@@ -224,7 +218,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
workspace: ProjectsSchema workspace: SanitizedProjectSchema
}) })
} }
}, },
@@ -272,7 +266,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspace: ProjectsSchema workspace: SanitizedProjectSchema
}) })
} }
}, },
@@ -314,7 +308,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
workspace: ProjectsSchema workspace: SanitizedProjectSchema
}) })
} }
}, },
@@ -351,7 +345,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
workspace: ProjectsSchema workspace: SanitizedProjectSchema
}) })
} }
}, },
@@ -389,7 +383,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
workspace: ProjectsSchema workspace: SanitizedProjectSchema
}) })
} }
}, },
@@ -8,6 +8,8 @@ import { readLimit, secretsLimit } 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";
import { secretRawSchema } from "../sanitizedSchemas";
export const registerSecretImportRouter = async (server: FastifyZodProvider) => { export const registerSecretImportRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
@@ -353,4 +355,48 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
return { secrets: importedSecrets }; return { secrets: importedSecrets };
} }
}); });
server.route({
url: "/secrets/raw",
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: 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 };
}
});
}; };
+18 -19
View File
@@ -8,25 +8,24 @@ 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 { WebhookType } from "@app/services/webhook/webhook-types"; import { WebhookType } from "@app/services/webhook/webhook-types";
export const sanitizedWebhookSchema = WebhooksSchema.omit({ export const sanitizedWebhookSchema = WebhooksSchema.pick({
encryptedSecretKey: true, id: true,
iv: true, secretPath: true,
tag: true, lastStatus: true,
algorithm: true, lastRunErrorMessage: true,
keyEncoding: true, isDisabled: true,
urlCipherText: true, createdAt: true,
urlIV: true, updatedAt: true,
urlTag: true envId: true,
}).merge( type: true
z.object({ }).extend({
projectId: z.string(), projectId: z.string(),
environment: z.object({ environment: z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
slug: z.string() slug: z.string()
})
}) })
); });
export const registerWebhookRouter = async (server: FastifyZodProvider) => { export const registerWebhookRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -228,7 +227,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
webhooks: sanitizedWebhookSchema.array() webhooks: sanitizedWebhookSchema.extend({ url: z.string() }).array()
}) })
} }
}, },
@@ -5,7 +5,6 @@ import {
IdentitiesSchema, IdentitiesSchema,
IdentityProjectMembershipsSchema, IdentityProjectMembershipsSchema,
ProjectMembershipRole, ProjectMembershipRole,
ProjectsSchema,
ProjectUserMembershipRolesSchema ProjectUserMembershipRolesSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { PROJECT_IDENTITIES } from "@app/lib/api-docs";
@@ -15,6 +14,8 @@ 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => { export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
@@ -236,7 +237,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
.array() .array()
}) })
@@ -294,7 +295,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
}) })
} }
+13 -11
View File
@@ -1,7 +1,7 @@
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas"; import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } 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 { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -12,12 +12,12 @@ import { CaStatus } from "@app/services/certificate-authority/certificate-author
import { ProjectFilterType } from "@app/services/project/project-types"; import { ProjectFilterType } from "@app/services/project/project-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
const projectWithEnv = ProjectsSchema.merge( import { SanitizedProjectSchema } from "../sanitizedSchemas";
z.object({
_id: z.string(), const projectWithEnv = SanitizedProjectSchema.extend({
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() _id: z.string(),
}) environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
); });
const slugSchema = z const slugSchema = z
.string() .string()
@@ -161,7 +161,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid slug" message: "Slug must be a valid slug"
}) })
.optional() .optional()
.describe(PROJECTS.CREATE.slug) .describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -177,7 +178,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName, workspaceName: req.body.projectName,
slug: req.body.slug slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId
}); });
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
@@ -212,7 +214,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
slug: slugSchema.describe("The slug of the project to delete.") slug: slugSchema.describe("The slug of the project to delete.")
}), }),
response: { response: {
200: ProjectsSchema 200: SanitizedProjectSchema
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -283,7 +285,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.") autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.")
}), }),
response: { response: {
200: ProjectsSchema 200: SanitizedProjectSchema
} }
}, },
+132 -43
View File
@@ -18,7 +18,7 @@ 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";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types"; import { ProjectFilterType } from "@app/services/project/project-types";
import { SecretOperations } from "@app/services/secret/secret-types"; import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas"; import { secretRawSchema } from "../sanitizedSchemas";
@@ -186,7 +186,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional() secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
})
.array()
.optional()
}) })
.array(), .array(),
imports: z imports: z
@@ -194,7 +202,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string(), secretPath: z.string(),
environment: z.string(), environment: z.string(),
folderId: z.string().optional(), folderId: z.string().optional(),
secrets: secretRawSchema.array() secrets: secretRawSchema.omit({ createdAt: true, updatedAt: true }).array()
}) })
.array() .array()
.optional() .optional()
@@ -425,17 +433,26 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds), tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type) type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type),
secretReminderRepeatDays: z
.number()
.optional()
.nullable()
.describe(RAW_SECRETS.CREATE.secretReminderRepeatDays),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote)
}), }),
response: { response: {
200: z.object({ 200: z.union([
secret: secretRawSchema z.object({
}) secret: secretRawSchema
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
} }
}, },
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 secret = await server.services.secret.createSecretRaw({ const secretOperation = await server.services.secret.createSecretRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
@@ -448,9 +465,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretValue: req.body.secretValue, secretValue: req.body.secretValue,
skipMultilineEncoding: req.body.skipMultilineEncoding, skipMultilineEncoding: req.body.skipMultilineEncoding,
secretComment: req.body.secretComment, secretComment: req.body.secretComment,
tagIds: req.body.tagIds tagIds: req.body.tagIds,
secretReminderNote: req.body.secretReminderNote,
secretReminderRepeatDays: req.body.secretReminderRepeatDays
}); });
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
...req.auditLogInfo, ...req.auditLogInfo,
@@ -514,17 +537,29 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.describe(RAW_SECRETS.UPDATE.secretPath), .describe(RAW_SECRETS.UPDATE.secretPath),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds) tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
metadata: z.record(z.string()).optional(),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretReminderRepeatDays: z
.number()
.optional()
.nullable()
.describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays),
newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName),
secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment)
}), }),
response: { response: {
200: z.object({ 200: z.union([
secret: secretRawSchema z.object({
}) secret: secretRawSchema
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
} }
}, },
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 secret = await server.services.secret.updateSecretRaw({ const secretOperation = await server.services.secret.updateSecretRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
@@ -536,8 +571,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
type: req.body.type, type: req.body.type,
secretValue: req.body.secretValue, secretValue: req.body.secretValue,
skipMultilineEncoding: req.body.skipMultilineEncoding, skipMultilineEncoding: req.body.skipMultilineEncoding,
tagIds: req.body.tagIds tagIds: req.body.tagIds,
secretReminderRepeatDays: req.body.secretReminderRepeatDays,
secretReminderNote: req.body.secretReminderNote,
metadata: req.body.metadata,
newSecretName: req.body.newSecretName,
secretComment: req.body.secretComment
}); });
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
@@ -598,14 +642,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.DELETE.type) type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.DELETE.type)
}), }),
response: { response: {
200: z.object({ 200: z.union([
secret: secretRawSchema z.object({
}) secret: secretRawSchema
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
} }
}, },
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 secret = await server.services.secret.deleteSecretRaw({ const secretOperation = await server.services.secret.deleteSecretRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -616,6 +663,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: req.params.secretName, secretName: req.params.secretName,
type: req.body.type type: req.body.type
}); });
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
@@ -1760,7 +1811,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
], ],
body: z.object({ body: z.object({
projectSlug: z.string().trim().describe(RAW_SECRETS.CREATE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId),
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
secretPath: z secretPath: z
.string() .string()
@@ -1776,22 +1828,27 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.CREATE.secretValue), .describe(RAW_SECRETS.CREATE.secretValue),
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding) skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
metadata: z.record(z.string()).optional(),
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds)
}) })
.array() .array()
.min(1) .min(1)
}), }),
response: { response: {
200: z.object({ 200: z.union([
secrets: secretRawSchema.array() z.object({
}) secrets: secretRawSchema.array()
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
} }
}, },
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 { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body;
const secrets = await server.services.secret.createManySecretsRaw({ const secretOperation = await server.services.secret.createManySecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -1799,8 +1856,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath, secretPath,
environment, environment,
projectSlug, projectSlug,
projectId: req.body.workspaceId,
secrets: inputSecrets secrets: inputSecrets
}); });
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
const { secrets } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace, projectId: secrets[0].workspace,
@@ -1810,9 +1872,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
secrets: secrets.map((secret, i) => ({ secrets: secrets.map((secret) => ({
secretId: secret.id, secretId: secret.id,
secretKey: inputSecrets[i].secretKey, secretKey: secret.secretKey,
secretVersion: secret.version secretVersion: secret.version
})) }))
} }
@@ -1849,7 +1911,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
], ],
body: z.object({ body: z.object({
projectSlug: z.string().trim().describe(RAW_SECRETS.UPDATE.projectSlug), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId),
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
secretPath: z secretPath: z
.string() .string()
@@ -1865,21 +1928,32 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.UPDATE.secretValue), .describe(RAW_SECRETS.UPDATE.secretValue),
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment), secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding) skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName),
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretReminderRepeatDays: z
.number()
.optional()
.nullable()
.describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays)
}) })
.array() .array()
.min(1) .min(1)
}), }),
response: { response: {
200: z.object({ 200: z.union([
secrets: secretRawSchema.array() z.object({
}) secrets: secretRawSchema.array()
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
} }
}, },
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 { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body;
const secrets = await server.services.secret.updateManySecretsRaw({ const secretOperation = await server.services.secret.updateManySecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -1887,8 +1961,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath, secretPath,
environment, environment,
projectSlug, projectSlug,
projectId: req.body.workspaceId,
secrets: inputSecrets secrets: inputSecrets
}); });
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
const { secrets } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace, projectId: secrets[0].workspace,
@@ -1898,9 +1977,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
secrets: secrets.map((secret, i) => ({ secrets: secrets.map((secret) => ({
secretId: secret.id, secretId: secret.id,
secretKey: inputSecrets[i].secretKey, secretKey: secret.secretKey,
secretVersion: secret.version secretVersion: secret.version
})) }))
} }
@@ -1937,7 +2016,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
], ],
body: z.object({ body: z.object({
projectSlug: z.string().trim().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),
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
secretPath: z secretPath: z
.string() .string()
@@ -1947,21 +2027,25 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.describe(RAW_SECRETS.DELETE.secretPath), .describe(RAW_SECRETS.DELETE.secretPath),
secrets: z secrets: z
.object({ .object({
secretKey: z.string().trim().describe(RAW_SECRETS.DELETE.secretName) secretKey: z.string().trim().describe(RAW_SECRETS.DELETE.secretName),
type: z.nativeEnum(SecretType).default(SecretType.Shared)
}) })
.array() .array()
.min(1) .min(1)
}), }),
response: { response: {
200: z.object({ 200: z.union([
secrets: secretRawSchema.array() z.object({
}) secrets: secretRawSchema.array()
}),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
])
} }
}, },
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 { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body; const { environment, projectSlug, secretPath, secrets: inputSecrets } = req.body;
const secrets = await server.services.secret.deleteManySecretsRaw({ const secretOperation = await server.services.secret.deleteManySecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -1969,8 +2053,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment, environment,
projectSlug, projectSlug,
secretPath, secretPath,
projectId: req.body.workspaceId,
secrets: inputSecrets secrets: inputSecrets
}); });
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
const { secrets } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace, projectId: secrets[0].workspace,
@@ -1980,9 +2069,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
secrets: secrets.map((secret, i) => ({ secrets: secrets.map((secret) => ({
secretId: secret.id, secretId: secret.id,
secretKey: inputSecrets[i].secretKey, secretKey: secret.secretKey,
secretVersion: secret.version secretVersion: secret.version
})) }))
} }
@@ -78,7 +78,7 @@ export const getCaCredentials = async ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const decryptedPrivateKey = kmsDecryptor({ const decryptedPrivateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
@@ -129,13 +129,13 @@ export const getCaCertChain = async ({
kmsId: keyId kmsId: keyId
}); });
const decryptedCaCert = kmsDecryptor({ const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate cipherTextBlob: caCert.encryptedCertificate
}); });
const caCertObj = new x509.X509Certificate(decryptedCaCert); const caCertObj = new x509.X509Certificate(decryptedCaCert);
const decryptedChain = kmsDecryptor({ const decryptedChain = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificateChain cipherTextBlob: caCert.encryptedCertificateChain
}); });
@@ -176,7 +176,7 @@ export const rebuildCaCrl = async ({
kmsId: keyId kmsId: keyId
}); });
const privateKey = kmsDecryptor({ const privateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
@@ -210,7 +210,7 @@ export const rebuildCaCrl = async ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData)) plainText: Buffer.from(new Uint8Array(crl.rawData))
}); });
@@ -91,7 +91,7 @@ export const certificateAuthorityQueueFactory = ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const privateKey = kmsDecryptor({ const privateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
@@ -125,7 +125,7 @@ export const certificateAuthorityQueueFactory = ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData)) plainText: Buffer.from(new Uint8Array(crl.rawData))
}); });
@@ -181,11 +181,11 @@ export const certificateAuthorityServiceFactory = ({
] ]
}); });
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(cert.rawData)) plainText: Buffer.from(new Uint8Array(cert.rawData))
}); });
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.alloc(0) plainText: Buffer.alloc(0)
}); });
@@ -209,7 +209,7 @@ export const certificateAuthorityServiceFactory = ({
signingKey: keys.privateKey signingKey: keys.privateKey
}); });
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData)) plainText: Buffer.from(new Uint8Array(crl.rawData))
}); });
@@ -224,7 +224,7 @@ export const certificateAuthorityServiceFactory = ({
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey); const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({ const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({ plainText: skObj.export({
type: "pkcs8", type: "pkcs8",
format: "der" format: "der"
@@ -458,7 +458,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const decryptedCaCert = kmsDecryptor({ const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate cipherTextBlob: caCert.encryptedCertificate
}); });
@@ -615,11 +615,11 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData)) plainText: Buffer.from(new Uint8Array(certObj.rawData))
}); });
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChain) plainText: Buffer.from(certificateChain)
}); });
@@ -693,7 +693,7 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const decryptedCaCert = kmsDecryptor({ const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate cipherTextBlob: caCert.encryptedCertificate
}); });
@@ -803,7 +803,7 @@ export const certificateAuthorityServiceFactory = ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData)) plainText: Buffer.from(new Uint8Array(leafCert.rawData))
}); });
@@ -173,7 +173,7 @@ export const certificateServiceFactory = ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId kmsId: certificateManagerKeyId
}); });
const decryptedCert = kmsDecryptor({ const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate cipherTextBlob: certBody.encryptedCertificate
}); });
@@ -11,7 +11,8 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list"; import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
@@ -53,8 +54,8 @@ type TIntegrationAuthServiceFactoryDep = {
integrationAuthDAL: TIntegrationAuthDALFactory; integrationAuthDAL: TIntegrationAuthDALFactory;
integrationDAL: Pick<TIntegrationDALFactory, "delete">; integrationDAL: Pick<TIntegrationDALFactory, "delete">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TIntegrationAuthServiceFactory = ReturnType<typeof integrationAuthServiceFactory>; export type TIntegrationAuthServiceFactory = ReturnType<typeof integrationAuthServiceFactory>;
@@ -63,8 +64,8 @@ export const integrationAuthServiceFactory = ({
permissionService, permissionService,
integrationAuthDAL, integrationAuthDAL,
integrationDAL, integrationDAL,
projectBotDAL, projectBotService,
projectBotService kmsService
}: TIntegrationAuthServiceFactoryDep) => { }: TIntegrationAuthServiceFactoryDep) => {
const listIntegrationAuthByProjectId = async ({ const listIntegrationAuthByProjectId = async ({
actorId, actorId,
@@ -122,9 +123,6 @@ export const integrationAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const bot = await projectBotDAL.findOne({ isActive: true, projectId });
if (!bot) throw new BadRequestError({ message: "Bot must be enabled for oauth2 code token exchange" });
const tokenExchange = await exchangeCode({ integration, code, url }); const tokenExchange = await exchangeCode({ integration, code, url });
const updateDoc: TIntegrationAuthsInsert = { const updateDoc: TIntegrationAuthsInsert = {
projectId, projectId,
@@ -145,18 +143,38 @@ export const integrationAuthServiceFactory = ({
}; };
} }
const key = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
if (tokenExchange.refreshToken) { if (shouldUseSecretV2Bridge) {
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenExchange.refreshToken, key); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
updateDoc.refreshIV = refreshEncToken.iv; type: KmsDataKey.SecretManager,
updateDoc.refreshTag = refreshEncToken.tag; projectId
updateDoc.refreshCiphertext = refreshEncToken.ciphertext; });
} if (tokenExchange.refreshToken) {
if (tokenExchange.accessToken) { const refreshEncToken = secretManagerEncryptor({
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenExchange.accessToken, key); plainText: Buffer.from(tokenExchange.refreshToken)
updateDoc.accessIV = accessEncToken.iv; }).cipherTextBlob;
updateDoc.accessTag = accessEncToken.tag; updateDoc.encryptedRefresh = refreshEncToken;
updateDoc.accessCiphertext = accessEncToken.ciphertext; }
if (tokenExchange.accessToken) {
const accessToken = secretManagerEncryptor({
plainText: Buffer.from(tokenExchange.accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessToken;
}
} else {
if (!botKey) throw new BadRequestError({ message: "Bot key not found" });
if (tokenExchange.refreshToken) {
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenExchange.refreshToken, botKey);
updateDoc.refreshIV = refreshEncToken.iv;
updateDoc.refreshTag = refreshEncToken.tag;
updateDoc.refreshCiphertext = refreshEncToken.ciphertext;
}
if (tokenExchange.accessToken) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenExchange.accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext;
}
} }
return integrationAuthDAL.transaction(async (tx) => { return integrationAuthDAL.transaction(async (tx) => {
const doc = await integrationAuthDAL.findOne({ projectId, integration }, tx); const doc = await integrationAuthDAL.findOne({ projectId, integration }, tx);
@@ -193,9 +211,6 @@ export const integrationAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const bot = await projectBotDAL.findOne({ isActive: true, projectId });
if (!bot) throw new BadRequestError({ message: "Bot must be enabled for oauth2 code token exchange" });
const updateDoc: TIntegrationAuthsInsert = { const updateDoc: TIntegrationAuthsInsert = {
projectId, projectId,
namespace, namespace,
@@ -212,109 +227,210 @@ export const integrationAuthServiceFactory = ({
: {}) : {})
}; };
const key = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
if (refreshToken) { if (shouldUseSecretV2Bridge) {
const tokenDetails = await exchangeRefresh( const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
integration, type: KmsDataKey.SecretManager,
refreshToken, projectId
url, });
updateDoc.metadata as Record<string, string> if (refreshToken) {
); const tokenDetails = await exchangeRefresh(
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, key); integration,
updateDoc.refreshIV = refreshEncToken.iv; refreshToken,
updateDoc.refreshTag = refreshEncToken.tag; url,
updateDoc.refreshCiphertext = refreshEncToken.ciphertext; updateDoc.metadata as Record<string, string>
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, key); );
updateDoc.accessIV = accessEncToken.iv; const refreshEncToken = secretManagerEncryptor({
updateDoc.accessTag = accessEncToken.tag; plainText: Buffer.from(tokenDetails.refreshToken)
updateDoc.accessCiphertext = accessEncToken.ciphertext; }).cipherTextBlob;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt; updateDoc.encryptedRefresh = refreshEncToken;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) { const accessEncToken = secretManagerEncryptor({
if (accessToken) { plainText: Buffer.from(tokenDetails.accessToken)
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, key); }).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
if (accessToken) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
}
if (accessId) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessId)
}).cipherTextBlob;
updateDoc.encryptedAccessId = accessEncToken;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({
plainText: Buffer.from(awsAssumeIamRoleArn)
}).cipherTextBlob;
updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted;
}
}
} else {
if (!botKey) throw new BadRequestError({ message: "Bot key not found" });
if (refreshToken) {
const tokenDetails = await exchangeRefresh(
integration,
refreshToken,
url,
updateDoc.metadata as Record<string, string>
);
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, botKey);
updateDoc.refreshIV = refreshEncToken.iv;
updateDoc.refreshTag = refreshEncToken.tag;
updateDoc.refreshCiphertext = refreshEncToken.ciphertext;
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv; updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag; updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext; updateDoc.accessCiphertext = accessEncToken.ciphertext;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
} }
if (accessId) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessId, key); if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
updateDoc.accessIdIV = accessEncToken.iv; if (accessToken) {
updateDoc.accessIdTag = accessEncToken.tag; const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, botKey);
updateDoc.accessIdCiphertext = accessEncToken.ciphertext; updateDoc.accessIV = accessEncToken.iv;
} updateDoc.accessTag = accessEncToken.tag;
if (awsAssumeIamRoleArn) { updateDoc.accessCiphertext = accessEncToken.ciphertext;
const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, key); }
updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext; if (accessId) {
updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv; const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessId, botKey);
updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag; updateDoc.accessIdIV = accessEncToken.iv;
updateDoc.accessIdTag = accessEncToken.tag;
updateDoc.accessIdCiphertext = accessEncToken.ciphertext;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, botKey);
updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext;
updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv;
updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag;
}
} }
} }
return integrationAuthDAL.create(updateDoc); return integrationAuthDAL.create(updateDoc);
}; };
// helper function // helper function
const getIntegrationAccessToken = async (integrationAuth: TIntegrationAuths, botKey: string) => { const getIntegrationAccessToken = async (
integrationAuth: TIntegrationAuths,
shouldUseSecretV2Bridge: boolean,
botKey?: string
) => {
let accessToken: string | undefined; let accessToken: string | undefined;
let accessId: string | undefined; let accessId: string | undefined;
// this means its not access token based // this means its not access token based
if ( if (
integrationAuth.integration === Integrations.AWS_SECRET_MANAGER && integrationAuth.integration === Integrations.AWS_SECRET_MANAGER &&
integrationAuth.awsAssumeIamRoleArnCipherText (shouldUseSecretV2Bridge
? integrationAuth.encryptedAwsAssumeIamRoleArn
: integrationAuth.awsAssumeIamRoleArnCipherText)
) { ) {
return { accessToken: "", accessId: "" }; return { accessToken: "", accessId: "" };
} }
if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: integrationAuth.projectId
});
if (integrationAuth.encryptedAccess) {
accessToken = secretManagerDecryptor({ cipherTextBlob: integrationAuth.encryptedAccess }).toString();
}
if (integrationAuth.accessTag && integrationAuth.accessIV && integrationAuth.accessCiphertext) { if (integrationAuth.encryptedRefresh) {
accessToken = decryptSymmetric128BitHexKeyUTF8({ const refreshToken = secretManagerDecryptor({ cipherTextBlob: integrationAuth.encryptedRefresh }).toString();
ciphertext: integrationAuth.accessCiphertext,
iv: integrationAuth.accessIV,
tag: integrationAuth.accessTag,
key: botKey
});
}
if (integrationAuth.refreshCiphertext && integrationAuth.refreshIV && integrationAuth.refreshTag) { if (integrationAuth.accessExpiresAt && integrationAuth.accessExpiresAt < new Date()) {
const refreshToken = decryptSymmetric128BitHexKeyUTF8({ // refer above it contains same logic except not saving
key: botKey, const tokenDetails = await exchangeRefresh(
ciphertext: integrationAuth.refreshCiphertext, integrationAuth.integration,
iv: integrationAuth.refreshIV, refreshToken,
tag: integrationAuth.refreshTag integrationAuth?.url,
}); integrationAuth.metadata as Record<string, string>
);
const encryptedRefresh = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.refreshToken)
}).cipherTextBlob;
const encryptedAccess = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.accessToken)
}).cipherTextBlob;
accessToken = tokenDetails.accessToken;
await integrationAuthDAL.updateById(integrationAuth.id, {
accessExpiresAt: tokenDetails.accessExpiresAt,
encryptedRefresh,
encryptedAccess
});
}
}
if (!accessToken) throw new BadRequestError({ message: "Missing access token" });
if (integrationAuth.accessExpiresAt && integrationAuth.accessExpiresAt < new Date()) { if (integrationAuth.encryptedAccessId) {
// refer above it contains same logic except not saving accessId = secretManagerDecryptor({
const tokenDetails = await exchangeRefresh( cipherTextBlob: integrationAuth.encryptedAccessId
integrationAuth.integration, }).toString();
refreshToken, }
integrationAuth?.url,
integrationAuth.metadata as Record<string, string> // the old bot key is else
); } else {
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, botKey); if (!botKey) throw new BadRequestError({ message: "bot key is missing" });
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, botKey); if (integrationAuth.accessTag && integrationAuth.accessIV && integrationAuth.accessCiphertext) {
accessToken = tokenDetails.accessToken; accessToken = decryptSymmetric128BitHexKeyUTF8({
await integrationAuthDAL.updateById(integrationAuth.id, { ciphertext: integrationAuth.accessCiphertext,
refreshIV: refreshEncToken.iv, iv: integrationAuth.accessIV,
refreshTag: refreshEncToken.tag, tag: integrationAuth.accessTag,
refreshCiphertext: refreshEncToken.ciphertext, key: botKey
accessExpiresAt: tokenDetails.accessExpiresAt, });
accessIV: accessEncToken.iv, }
accessTag: accessEncToken.tag,
accessCiphertext: accessEncToken.ciphertext if (integrationAuth.refreshCiphertext && integrationAuth.refreshIV && integrationAuth.refreshTag) {
const refreshToken = decryptSymmetric128BitHexKeyUTF8({
key: botKey,
ciphertext: integrationAuth.refreshCiphertext,
iv: integrationAuth.refreshIV,
tag: integrationAuth.refreshTag
});
if (integrationAuth.accessExpiresAt && integrationAuth.accessExpiresAt < new Date()) {
// refer above it contains same logic except not saving
const tokenDetails = await exchangeRefresh(
integrationAuth.integration,
refreshToken,
integrationAuth?.url,
integrationAuth.metadata as Record<string, string>
);
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, botKey);
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, botKey);
accessToken = tokenDetails.accessToken;
await integrationAuthDAL.updateById(integrationAuth.id, {
refreshIV: refreshEncToken.iv,
refreshTag: refreshEncToken.tag,
refreshCiphertext: refreshEncToken.ciphertext,
accessExpiresAt: tokenDetails.accessExpiresAt,
accessIV: accessEncToken.iv,
accessTag: accessEncToken.tag,
accessCiphertext: accessEncToken.ciphertext
});
}
}
if (!accessToken) throw new BadRequestError({ message: "Missing access token" });
if (integrationAuth.accessIdTag && integrationAuth.accessIdIV && integrationAuth.accessIdCiphertext) {
accessId = decryptSymmetric128BitHexKeyUTF8({
key: botKey,
ciphertext: integrationAuth.accessIdCiphertext,
iv: integrationAuth.accessIdIV,
tag: integrationAuth.accessIdTag
}); });
} }
} }
if (!accessToken) throw new BadRequestError({ message: "Missing access token" });
if (integrationAuth.accessIdTag && integrationAuth.accessIdIV && integrationAuth.accessIdCiphertext) {
accessId = decryptSymmetric128BitHexKeyUTF8({
key: botKey,
ciphertext: integrationAuth.accessIdCiphertext,
iv: integrationAuth.accessIdIV,
tag: integrationAuth.accessIdTag
});
}
return { accessId, accessToken }; return { accessId, accessToken };
}; };
@@ -339,8 +455,8 @@ export const integrationAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken, accessId } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken, accessId } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const apps = await getApps({ const apps = await getApps({
integration: integrationAuth.integration, integration: integrationAuth.integration,
accessToken, accessToken,
@@ -371,8 +487,8 @@ export const integrationAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const teams = await getTeams({ const teams = await getTeams({
integration: integrationAuth.integration, integration: integrationAuth.integration,
accessToken, accessToken,
@@ -400,8 +516,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (appId) { if (appId) {
const { data } = await request.get<TVercelBranches[]>( const { data } = await request.get<TVercelBranches[]>(
@@ -441,8 +557,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (accountId) { if (accountId) {
const { data } = await request.get<TChecklyGroups[]>(`${IntegrationUrls.CHECKLY_API_URL}/v1/check-groups`, { const { data } = await request.get<TChecklyGroups[]>(`${IntegrationUrls.CHECKLY_API_URL}/v1/check-groups`, {
headers: { headers: {
@@ -468,8 +584,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken auth: accessToken
@@ -505,8 +621,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken auth: accessToken
@@ -537,8 +653,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const { data } = await request.get<{ results: Array<{ id: string; name: string }> }>( const { data } = await request.get<{ results: Array<{ id: string; name: string }> }>(
`${IntegrationUrls.QOVERY_API_URL}/organization`, `${IntegrationUrls.QOVERY_API_URL}/organization`,
{ {
@@ -571,8 +687,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const kms = new AWS.KMS({ const kms = new AWS.KMS({
region, region,
@@ -629,8 +745,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (orgId) { if (orgId) {
const { data } = await request.get<{ results: Array<{ id: string; name: string }> }>( const { data } = await request.get<{ results: Array<{ id: string; name: string }> }>(
`${IntegrationUrls.QOVERY_API_URL}/organization/${orgId}/project`, `${IntegrationUrls.QOVERY_API_URL}/organization/${orgId}/project`,
@@ -665,8 +781,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (projectId && projectId !== "none") { if (projectId && projectId !== "none") {
// TODO: fix // TODO: fix
const { data } = await request.get<{ results: { id: string; name: string }[] }>( const { data } = await request.get<{ results: { id: string; name: string }[] }>(
@@ -706,8 +822,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (environmentId) { if (environmentId) {
const { data } = await request.get<{ results: { id: string; name: string }[] }>( const { data } = await request.get<{ results: { id: string; name: string }[] }>(
`${IntegrationUrls.QOVERY_API_URL}/environment/${environmentId}/application`, `${IntegrationUrls.QOVERY_API_URL}/environment/${environmentId}/application`,
@@ -746,8 +862,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (environmentId) { if (environmentId) {
const { data } = await request.get<{ results: { id: string; name: string }[] }>( const { data } = await request.get<{ results: { id: string; name: string }[] }>(
`${IntegrationUrls.QOVERY_API_URL}/environment/${environmentId}/container`, `${IntegrationUrls.QOVERY_API_URL}/environment/${environmentId}/container`,
@@ -786,8 +902,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (environmentId) { if (environmentId) {
const { data } = await request.get<{ results: { id: string; name: string }[] }>( const { data } = await request.get<{ results: { id: string; name: string }[] }>(
`${IntegrationUrls.QOVERY_API_URL}/environment/${environmentId}/job`, `${IntegrationUrls.QOVERY_API_URL}/environment/${environmentId}/job`,
@@ -825,8 +941,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const { data } = await request.get<THerokuPipelineCoupling[]>( const { data } = await request.get<THerokuPipelineCoupling[]>(
`${IntegrationUrls.HEROKU_API_URL}/pipeline-couplings`, `${IntegrationUrls.HEROKU_API_URL}/pipeline-couplings`,
@@ -865,8 +981,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (appId) { if (appId) {
const query = ` const query = `
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) { query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
@@ -933,8 +1049,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (appId && appId !== "") { if (appId && appId !== "") {
const query = ` const query = `
@@ -1007,8 +1123,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const workspaces: TBitbucketWorkspace[] = []; const workspaces: TBitbucketWorkspace[] = [];
let hasNextPage = true; let hasNextPage = true;
let workspaceUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces`; let workspaceUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces`;
@@ -1056,8 +1172,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const secretGroups: { name: string; groupId: string }[] = []; const secretGroups: { name: string; groupId: string }[] = [];
if (appId) { if (appId) {
@@ -1124,8 +1240,8 @@ export const integrationAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
if (appId) { if (appId) {
const { const {
data: { buildType } data: { buildType }
@@ -26,7 +26,7 @@ import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { z } from "zod"; import { z } from "zod";
import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas"; import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -275,8 +275,8 @@ const syncSecretsAzureKeyVault = async ({
}; };
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessToken: string; accessToken: string;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<{ id: string }>>;
}) => { }) => {
interface GetAzureKeyVaultSecret { interface GetAzureKeyVaultSecret {
id: string; // secret URI id: string; // secret URI
@@ -903,8 +903,8 @@ const syncSecretsHeroku = async ({
secrets, secrets,
accessToken accessToken
}: { }: {
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<{ id: string }>>;
integration: TIntegrations & { integration: TIntegrations & {
projectId: string; projectId: string;
environment: { environment: {
@@ -2464,8 +2464,8 @@ const syncSecretsTerraformCloud = async ({
accessToken, accessToken,
integrationDAL integrationDAL
}: { }: {
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<{ id: string }>>;
integration: TIntegrations & { integration: TIntegrations & {
projectId: string; projectId: string;
environment: { environment: {
@@ -3612,8 +3612,8 @@ export const syncIntegrationSecrets = async ({
appendices, appendices,
projectId projectId
}: { }: {
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>; updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<{ id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">; integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
integration: TIntegrations & { integration: TIntegrations & {
projectId: string; projectId: string;
@@ -123,7 +123,11 @@ export const integrationDALFactory = (db: TDbClient) => {
db.ref("keyEncoding").withSchema(TableName.IntegrationAuth).as("keyEncodingAu"), db.ref("keyEncoding").withSchema(TableName.IntegrationAuth).as("keyEncodingAu"),
db.ref("awsAssumeIamRoleArnCipherText").withSchema(TableName.IntegrationAuth), db.ref("awsAssumeIamRoleArnCipherText").withSchema(TableName.IntegrationAuth),
db.ref("awsAssumeIamRoleArnIV").withSchema(TableName.IntegrationAuth), db.ref("awsAssumeIamRoleArnIV").withSchema(TableName.IntegrationAuth),
db.ref("awsAssumeIamRoleArnTag").withSchema(TableName.IntegrationAuth) db.ref("awsAssumeIamRoleArnTag").withSchema(TableName.IntegrationAuth),
db.ref("encryptedRefresh").withSchema(TableName.IntegrationAuth),
db.ref("encryptedAccess").withSchema(TableName.IntegrationAuth),
db.ref("encryptedAccessId").withSchema(TableName.IntegrationAuth),
db.ref("encryptedAwsAssumeIamRoleArn").withSchema(TableName.IntegrationAuth)
); );
return docs.map( return docs.map(
({ ({
@@ -152,6 +156,10 @@ export const integrationDALFactory = (db: TDbClient) => {
awsAssumeIamRoleArnIV, awsAssumeIamRoleArnIV,
awsAssumeIamRoleArnCipherText, awsAssumeIamRoleArnCipherText,
awsAssumeIamRoleArnTag, awsAssumeIamRoleArnTag,
encryptedAccess,
encryptedRefresh,
encryptedAccessId,
encryptedAwsAssumeIamRoleArn,
...el ...el
}) => ({ }) => ({
...el, ...el,
@@ -183,7 +191,11 @@ export const integrationDALFactory = (db: TDbClient) => {
accessExpiresAt, accessExpiresAt,
awsAssumeIamRoleArnIV, awsAssumeIamRoleArnIV,
awsAssumeIamRoleArnCipherText, awsAssumeIamRoleArnCipherText,
awsAssumeIamRoleArnTag awsAssumeIamRoleArnTag,
encryptedAccess,
encryptedRefresh,
encryptedAccessId,
encryptedAwsAssumeIamRoleArn
} }
}) })
); );
+11
View File
@@ -10,10 +10,13 @@ export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
export const kmskeyDALFactory = (db: TDbClient) => { export const kmskeyDALFactory = (db: TDbClient) => {
const kmsOrm = ormify(db, TableName.KmsKey); const kmsOrm = ormify(db, TableName.KmsKey);
// akhilmhdh: this function should never be called outside kms service
// why: because the encrypted key should never be shared with another service
const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => { const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => {
try { try {
const result = await (tx || db.replicaNode())(TableName.KmsKey) const result = await (tx || db.replicaNode())(TableName.KmsKey)
.where({ [`${TableName.KmsKey}.id` as "id"]: id }) .where({ [`${TableName.KmsKey}.id` as "id"]: id })
.join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`)
.leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
.leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`) .leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
.first() .first()
@@ -31,11 +34,19 @@ export const kmskeyDALFactory = (db: TDbClient) => {
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"), db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"), db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails") db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails")
)
.select(
db.ref("kmsDefaultKeyId").withSchema(TableName.Organization).as("orgKmsDefaultKeyId"),
db.ref("kmsEncryptedDataKey").withSchema(TableName.Organization).as("orgKmsEncryptedDataKey")
); );
const data = { const data = {
...KmsKeysSchema.parse(result), ...KmsKeysSchema.parse(result),
isExternal: Boolean(result?.externalKmsId), isExternal: Boolean(result?.externalKmsId),
orgKms: {
id: result?.orgKmsDefaultKeyId,
encryptedDataKey: result?.orgKmsEncryptedDataKey
},
externalKms: result?.externalKmsId externalKms: result?.externalKmsId
? { ? {
id: result.externalKmsId, id: result.externalKmsId,
+636 -73
View File
@@ -1,11 +1,20 @@
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { Knex } from "knex"; import { Knex } from "knex";
import { z } from "zod";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { KmsKeysSchema } from "@app/db/schemas";
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
import {
ExternalKmsAwsSchema,
KmsProviders,
TExternalKmsProviderFns
} from "@app/ee/services/external-kms/providers/model";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto"; import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { BadRequestError } from "@app/lib/errors"; import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -15,11 +24,15 @@ import { TInternalKmsDALFactory } from "./internal-kms-dal";
import { TKmsKeyDALFactory } from "./kms-key-dal"; import { TKmsKeyDALFactory } from "./kms-key-dal";
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import { import {
KmsDataKey,
KmsType,
TDecryptWithKeyDTO, TDecryptWithKeyDTO,
TDecryptWithKmsDTO, TDecryptWithKmsDTO,
TEncryptionWithKeyDTO, TEncryptionWithKeyDTO,
TEncryptWithKmsDataKeyDTO,
TEncryptWithKmsDTO, TEncryptWithKmsDTO,
TGenerateKMSDTO TGenerateKMSDTO,
TUpdateProjectSecretManagerKmsKeyDTO
} from "./kms-types"; } from "./kms-types";
type TKmsServiceFactoryDep = { type TKmsServiceFactoryDep = {
@@ -41,6 +54,8 @@ const KMS_ROOT_CREATION_WAIT_TIME = 10;
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms // akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
const KMS_VERSION = "v01"; const KMS_VERSION = "v01";
const KMS_VERSION_BLOB_LENGTH = 3; const KMS_VERSION_BLOB_LENGTH = 3;
const KmsSanitizedSchema = KmsKeysSchema.extend({ isExternal: z.boolean() });
export const kmsServiceFactory = ({ export const kmsServiceFactory = ({
kmsDAL, kmsDAL,
kmsRootConfigDAL, kmsRootConfigDAL,
@@ -51,7 +66,11 @@ export const kmsServiceFactory = ({
}: TKmsServiceFactoryDep) => { }: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
// this is used symmetric encryption /*
* Generate KMS Key
* This function is responsibile for generating the infisical internal KMS for various entities
* Like for secret manager, cert manager or for organization
*/
const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => { const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKeyMaterial = randomSecureBytes(32); const kmsKeyMaterial = randomSecureBytes(32);
@@ -83,22 +102,18 @@ export const kmsServiceFactory = ({
return doc; return doc;
}; };
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">) => { const deleteInternalKms = async (kmsId: string, orgId: string, tx?: Knex) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); const kms = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); if (kms.isExternal) return;
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm if (kms.orgId !== orgId) throw new BadRequestError({ message: "KMS doesn't belong to organization" });
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); return kmsDAL.deleteById(kmsId, tx);
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
};
}; };
/*
* Simple encryption service function to do all the encryption tasks in infisical
* This can be even later exposed directly as api for encryption as function
* The encrypted binary even has everything into it. The IV, the version etc
*/
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => { const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
@@ -111,19 +126,10 @@ export const kmsServiceFactory = ({
}; };
}; };
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => { /*
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); * Simple decryption service function to do all the encryption tasks in infisical
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); * This can be even later exposed directly as api for encryption as function
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); */
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
return decryptedBlob;
};
};
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => { const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
@@ -134,70 +140,621 @@ export const kmsServiceFactory = ({
}; };
}; };
/*
* Function to generate a KMS for an org
* We handle concurrent with redis locking and waitReady
* What happens is first we check kms is assigned else first we acquire lock and create the kms with connection
* In mean time the rest of the request will wait until creation is finished followed by getting the created on
* In real time this would be milliseconds
*/
const getOrgKmsKeyId = async (orgId: string) => { const getOrgKmsKeyId = async (orgId: string) => {
const keyId = await orgDAL.transaction(async (tx) => { let org = await orgDAL.findById(orgId);
const org = await orgDAL.findById(orgId, tx);
if (!org) { if (!org) {
throw new BadRequestError({ message: "Org not found" }); throw new NotFoundError({ message: "Org not found" });
}
if (!org.kmsDefaultKeyId) {
const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsOrgKeyCreation, orgId], 3000, { retryCount: 3 })
.catch(() => null);
try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyKmsOrgKeyCreation}${orgId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.info("KMS. Waiting for org key to be created")
});
org = await orgDAL.findById(orgId);
} else {
const keyId = await orgDAL.transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx);
if (org.kmsDefaultKeyId) {
return org.kmsDefaultKeyId;
}
const key = await generateKmsKey({
isReserved: true,
orgId: org.id,
tx
});
await orgDAL.updateById(
org.id,
{
kmsDefaultKeyId: key.id
},
tx
);
await keyStore.setItemWithExpiry(`${KeyStorePrefixes.WaitUntilReadyKmsOrgKeyCreation}${orgId}`, 10, "true");
return key.id;
});
return keyId;
}
} finally {
await lock?.release();
}
}
if (!org.kmsDefaultKeyId) {
throw new Error("Invalid organization KMS");
}
return org.kmsDefaultKeyId;
};
const decryptWithKmsKey = async ({
kmsId,
depth = 0
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number }) => {
if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" });
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found" });
}
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
throw new Error("Invalid organization KMS");
} }
if (!org.kmsDefaultKeyId) { // The idea is external kms connection info is encrypted by an org default KMS
// create default kms key for certificate service // This could be external kms(in future) but at the end of the day, the end KMS will be an infisical internal one
const key = await generateKmsKey({ // we put a limit of depth to avoid too many cycles
isReserved: true, const orgKmsDecryptor = await decryptWithKmsKey({
orgId: org.id, kmsId: kmsDoc.orgKms.id,
tx depth: depth + 1
}); });
await orgDAL.updateById( const orgKmsDataKey = await orgKmsDecryptor({
org.id, cipherTextBlob: kmsDoc.orgKms.encryptedDataKey
{ });
kmsDefaultKeyId: key.id
},
tx
);
return key.id; const kmsDecryptor = await decryptWithInputKey({
key: orgKmsDataKey
});
const decryptedProviderInputBlob = kmsDecryptor({
cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput
});
switch (kmsDoc.externalKms.provider) {
case KmsProviders.Aws: {
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
);
externalKms = await AwsKmsProviderFactory({
inputs: decryptedProviderInput
});
break;
}
default:
throw new Error("Invalid KMS provider.");
} }
return org.kmsDefaultKeyId; return async ({ cipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const { data } = await externalKms.decrypt(cipherTextBlob);
return data;
};
}
// internal KMS
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
return Promise.resolve(decryptedBlob);
};
};
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found" });
}
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
throw new Error("Invalid organization KMS");
}
const orgKmsDecryptor = await decryptWithKmsKey({
kmsId: kmsDoc.orgKms.id
});
const orgKmsDataKey = await orgKmsDecryptor({
cipherTextBlob: kmsDoc.orgKms.encryptedDataKey
});
const kmsDecryptor = await decryptWithInputKey({
key: orgKmsDataKey
});
const decryptedProviderInputBlob = kmsDecryptor({
cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput
});
switch (kmsDoc.externalKms.provider) {
case KmsProviders.Aws: {
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
);
externalKms = await AwsKmsProviderFactory({
inputs: decryptedProviderInput
});
break;
}
default:
throw new Error("Invalid KMS provider.");
}
return async ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const { encryptedBlob } = await externalKms.encrypt(plainText);
return { cipherTextBlob: encryptedBlob };
};
}
// internal KMS
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return Promise.resolve({ cipherTextBlob });
};
};
const $getOrgKmsDataKey = async (orgId: string) => {
const kmsKeyId = await getOrgKmsKeyId(orgId);
let org = await orgDAL.findById(orgId);
if (!org) {
throw new NotFoundError({ message: "Org not found" });
}
if (!org.kmsEncryptedDataKey) {
const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsOrgDataKeyCreation, orgId], 500, { retryCount: 0 })
.catch(() => null);
try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyKmsOrgDataKeyCreation}${orgId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.info("KMS. Waiting for org data key to be created")
});
org = await orgDAL.findById(orgId);
} else {
const orgDataKey = await orgDAL.transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx);
if (org.kmsEncryptedDataKey) {
return;
}
const dataKey = randomSecureBytes();
const kmsEncryptor = await encryptWithKmsKey(
{
kmsId: kmsKeyId
},
tx
);
const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey
});
await orgDAL.updateById(
org.id,
{
kmsEncryptedDataKey: cipherTextBlob
},
tx
);
await keyStore.setItemWithExpiry(
`${KeyStorePrefixes.WaitUntilReadyKmsOrgDataKeyCreation}${orgId}`,
10,
"true"
);
return dataKey;
});
if (orgDataKey) {
return orgDataKey;
}
}
} finally {
await lock?.release();
}
}
if (!org.kmsEncryptedDataKey) {
throw new Error("Invalid organization KMS");
}
const kmsDecryptor = await decryptWithKmsKey({
kmsId: kmsKeyId
}); });
return keyId; return kmsDecryptor({
cipherTextBlob: org.kmsEncryptedDataKey
});
}; };
const getProjectSecretManagerKmsKeyId = async (projectId: string) => { const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
const keyId = await projectDAL.transaction(async (tx) => { let project = await projectDAL.findById(projectId);
const project = await projectDAL.findById(projectId, tx); if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
if (!project.kmsSecretManagerKeyId) {
const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 0 })
.catch(() => null);
try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.debug("KMS. Waiting for project key to be created"),
delay: 500
});
project = await projectDAL.findById(projectId);
} else {
const kmsKeyId = await projectDAL.transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerKeyId) {
return project.kmsSecretManagerKeyId;
}
const key = await generateKmsKey({
isReserved: true,
orgId: project.orgId,
tx
});
await projectDAL.updateById(
projectId,
{
kmsSecretManagerKeyId: key.id
},
tx
);
return key.id;
});
await keyStore.setItemWithExpiry(
`${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`,
10,
"true"
);
return kmsKeyId;
}
} finally {
await lock?.release();
}
}
if (!project.kmsSecretManagerKeyId) {
throw new Error("Missing project KMS key ID");
}
return project.kmsSecretManagerKeyId;
};
const $getProjectSecretManagerKmsDataKey = async (projectId: string) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
let project = await projectDAL.findById(projectId);
if (!project.kmsSecretManagerEncryptedDataKey) {
const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsProjectDataKeyCreation, projectId], 3000, { retryCount: 0 })
.catch(() => null);
try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.debug("KMS. Waiting for secret manager data key to be created"),
delay: 500
});
project = await projectDAL.findById(projectId);
} else {
const projectDataKey = await projectDAL.transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerEncryptedDataKey) {
return;
}
const dataKey = randomSecureBytes();
const kmsEncryptor = await encryptWithKmsKey({
kmsId: kmsKeyId
});
const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey
});
await projectDAL.updateById(
projectId,
{
kmsSecretManagerEncryptedDataKey: cipherTextBlob
},
tx
);
await keyStore.setItemWithExpiry(
`${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`,
10,
"true"
);
return dataKey;
});
if (projectDataKey) {
return projectDataKey;
}
}
} finally {
await lock?.release();
}
}
if (!project.kmsSecretManagerEncryptedDataKey) {
throw new Error("Missing project data key");
}
const kmsDecryptor = await decryptWithKmsKey({
kmsId: kmsKeyId
});
return kmsDecryptor({
cipherTextBlob: project.kmsSecretManagerEncryptedDataKey
});
};
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => {
switch (dto.type) {
case KmsDataKey.SecretManager: {
return $getProjectSecretManagerKmsDataKey(dto.projectId);
}
default: {
return $getOrgKmsDataKey(dto.orgId);
}
}
};
// by keeping the decrypted data key in inner scope
// none of the entities outside can interact directly or expose the data key
// NOTICE: If changing here update migrations/utils/kms
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
const dataKey = await $getDataKey(encryptionContext);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return {
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
},
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
return decryptedBlob;
}
};
};
const updateProjectSecretManagerKmsKey = async ({ projectId, kms }: TUpdateProjectSecretManagerKmsKeyDTO) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
const currentKms = await kmsDAL.findById(kmsKeyId);
// case: internal kms -> internal kms. no change needed
if (kms.type === KmsType.Internal && currentKms.isReserved) {
return KmsSanitizedSchema.parseAsync({ isExternal: false, ...currentKms });
}
if (kms.type === KmsType.External) {
// validate kms is scoped in org
const { kmsId } = kms;
const project = await projectDAL.findById(projectId);
if (!project) { if (!project) {
throw new BadRequestError({ message: "Project not found" }); throw new NotFoundError({
message: "Project not found."
});
}
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found." });
} }
if (!project.kmsSecretManagerKeyId) { if (kmsDoc.orgId !== project.orgId) {
// create default kms key for certificate service throw new BadRequestError({
const key = await generateKmsKey({ message: "KMS ID does not belong in the organization."
});
}
}
const dataKey = await $getProjectSecretManagerKmsDataKey(projectId);
return kmsDAL.transaction(async (tx) => {
const project = await projectDAL.findById(projectId, tx);
let kmsId;
if (kms.type === KmsType.Internal) {
const internalKms = await generateKmsKey({
isReserved: true, isReserved: true,
orgId: project.orgId, orgId: project.orgId,
tx tx
}); });
kmsId = internalKms.id;
await projectDAL.updateById( } else {
projectId, kmsId = kms.kmsId;
{
kmsSecretManagerKeyId: key.id
},
tx
);
return key.id;
} }
return project.kmsSecretManagerKeyId; const kmsEncryptor = await encryptWithKmsKey({ kmsId }, tx);
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
await projectDAL.updateById(
projectId,
{
kmsSecretManagerKeyId: kmsId,
kmsSecretManagerEncryptedDataKey: cipherTextBlob
},
tx
);
if (currentKms.isReserved) {
await kmsDAL.deleteById(currentKms.id, tx);
}
const newKms = await kmsDAL.findById(kmsId, tx);
return KmsSanitizedSchema.parseAsync({ isExternal: !currentKms.isReserved, ...newKms });
}); });
return keyId;
}; };
const getProjectKeyBackup = async (projectId: string) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const secretManagerDataKey = await $getProjectSecretManagerKmsDataKey(projectId);
const kmsKeyIdForEncrypt = await getOrgKmsKeyId(project.orgId);
const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt });
const { cipherTextBlob: encryptedSecretManagerDataKeyWithOrgKms } = await kmsEncryptor({
plainText: secretManagerDataKey
});
// backup format: version.projectId.kmsFunction.kmsId.Base64(encryptedDataKey).verificationHash
let secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKeyWithOrgKms.toString(
"base64"
)}`;
const verificationHash = generateHash(secretManagerBackup);
secretManagerBackup = `${secretManagerBackup}.${verificationHash}`;
return {
secretManager: secretManagerBackup
};
};
const loadProjectKeyBackup = async (projectId: string, backup: string) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const [, backupProjectId, , backupKmsKeyId, backupBase64EncryptedDataKey, backupHash] = backup.split(".");
const computedHash = generateHash(backup.substring(0, backup.lastIndexOf(".")));
if (computedHash !== backupHash) {
throw new BadRequestError({
message: "Invalid backup"
});
}
if (backupProjectId !== projectId) {
throw new BadRequestError({
message: "Invalid backup for project"
});
}
const kmsDecryptor = await decryptWithKmsKey({ kmsId: backupKmsKeyId });
const dataKey = await kmsDecryptor({
cipherTextBlob: Buffer.from(backupBase64EncryptedDataKey, "base64")
});
const newKms = await kmsDAL.transaction(async (tx) => {
const key = await generateKmsKey({
isReserved: true,
orgId: project.orgId,
tx
});
const kmsEncryptor = await encryptWithKmsKey({ kmsId: key.id }, tx);
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
await projectDAL.updateById(
projectId,
{
kmsSecretManagerKeyId: key.id,
kmsSecretManagerEncryptedDataKey: cipherTextBlob
},
tx
);
return kmsDAL.findByIdWithAssociatedKms(key.id, tx);
});
return {
secretManagerKmsKey: newKms
};
};
const getKmsById = async (kmsKeyId: string, tx?: Knex) => {
const kms = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId, tx);
if (!kms.id) {
throw new NotFoundError({
message: "KMS not found"
});
}
const { id, slug, orgId, isExternal } = kms;
return { id, slug, orgId, isExternal };
};
// akhilmhdh: a copy of this is made in migrations/utils/kms
const startService = async () => { const startService = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future // This will switch to a seal process and HMS flow in future
@@ -246,11 +803,17 @@ export const kmsServiceFactory = ({
return { return {
startService, startService,
generateKmsKey, generateKmsKey,
deleteInternalKms,
encryptWithKmsKey, encryptWithKmsKey,
encryptWithInputKey,
decryptWithKmsKey, decryptWithKmsKey,
encryptWithInputKey,
decryptWithInputKey, decryptWithInputKey,
getOrgKmsKeyId, getOrgKmsKeyId,
getProjectSecretManagerKmsKeyId getProjectSecretManagerKmsKeyId,
updateProjectSecretManagerKmsKey,
getProjectKeyBackup,
loadProjectKeyBackup,
getKmsById,
createCipherPairWithDataKey
}; };
}; };
+25
View File
@@ -1,5 +1,25 @@
import { Knex } from "knex"; import { Knex } from "knex";
export enum KmsDataKey {
Organization,
SecretManager
// CertificateManager
}
export enum KmsType {
External = "external",
Internal = "internal"
}
export type TEncryptWithKmsDataKeyDTO =
| { type: KmsDataKey.Organization; orgId: string }
| { type: KmsDataKey.SecretManager; projectId: string };
// akhilmhdh: not implemented yet
// | {
// type: KmsDataKey.CertificateManager;
// projectId: string;
// };
export type TGenerateKMSDTO = { export type TGenerateKMSDTO = {
orgId: string; orgId: string;
isReserved?: boolean; isReserved?: boolean;
@@ -26,3 +46,8 @@ export type TDecryptWithKeyDTO = {
key: Buffer; key: Buffer;
cipherTextBlob: Buffer; cipherTextBlob: Buffer;
}; };
export type TUpdateProjectSecretManagerKmsKeyDTO = {
projectId: string;
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
};
@@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TProjectBots } from "@app/db/schemas"; import { TableName, TProjectBots, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
@@ -41,5 +41,43 @@ export const projectBotDALFactory = (db: TDbClient) => {
} }
}; };
return { ...projectBotOrm, findOne, findProjectByBotId }; const findProjectUserWorkspaceKey = async (projectId: string) => {
try {
const doc = await db
.replicaNode()(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
.where(`${TableName.Users}.isGhost` as "isGhost", false)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
.join<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.join<TUserEncryptionKeys>(
db(TableName.UserEncryptionKey).as("senderUserEncryption"),
`${TableName.ProjectKeys}.senderId`,
`senderUserEncryption.userId`
)
.whereNotNull(`${TableName.UserEncryptionKey}.serverEncryptedPrivateKey`)
.whereNotNull(`${TableName.UserEncryptionKey}.serverEncryptedPrivateKeyIV`)
.whereNotNull(`${TableName.UserEncryptionKey}.serverEncryptedPrivateKeyTag`)
.select(
db.ref("serverEncryptedPrivateKey").withSchema(TableName.UserEncryptionKey),
db.ref("serverEncryptedPrivateKeyTag").withSchema(TableName.UserEncryptionKey),
db.ref("serverEncryptedPrivateKeyIV").withSchema(TableName.UserEncryptionKey),
db.ref("serverEncryptedPrivateKeyEncoding").withSchema(TableName.UserEncryptionKey),
db.ref("encryptedKey").withSchema(TableName.ProjectKeys).as("projectEncryptedKey"),
db.ref("nonce").withSchema(TableName.ProjectKeys).as("projectKeyNonce"),
db.ref("publicKey").withSchema("senderUserEncryption").as("senderPublicKey"),
db.ref("id").withSchema(TableName.Users).as("userId")
)
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find all project members" });
}
};
return { ...projectBotOrm, findOne, findProjectByBotId, findProjectUserWorkspaceKey };
}; };
@@ -1,5 +1,11 @@
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretKeyEncoding } from "@app/db/schemas";
import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import {
decryptAsymmetric,
encryptAsymmetric,
generateAsymmetricKeyPair,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -22,21 +28,75 @@ export const getBotKeyFnFactory = (
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." }); if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." });
const bot = await projectBotDAL.findOne({ projectId: project.id }); if (project.version === 3) {
return { project, shouldUseSecretV2Bridge: true };
}
if (!bot) throw new BadRequestError({ message: "Failed to find bot key", name: "bot_not_found_error" }); const bot = await projectBotDAL.findOne({ projectId: project.id });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active", name: "bot_not_found_error" }); if (!bot || !bot.isActive || !bot.encryptedProjectKey || !bot.encryptedProjectKeyNonce) {
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey) // trying to set bot automatically
throw new BadRequestError({ message: "Encryption key missing", name: "bot_not_found_error" }); const projectV1Keys = await projectBotDAL.findProjectUserWorkspaceKey(projectId);
if (!projectV1Keys) throw new BadRequestError({ message: "Bot not found. Please ask admin user to login" });
let userPrivateKey = "";
if (
projectV1Keys?.serverEncryptedPrivateKey &&
projectV1Keys.serverEncryptedPrivateKeyIV &&
projectV1Keys.serverEncryptedPrivateKeyTag &&
projectV1Keys.serverEncryptedPrivateKeyEncoding
) {
userPrivateKey = infisicalSymmetricDecrypt({
iv: projectV1Keys.serverEncryptedPrivateKeyIV,
tag: projectV1Keys.serverEncryptedPrivateKeyTag,
ciphertext: projectV1Keys.serverEncryptedPrivateKey,
keyEncoding: projectV1Keys.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
}
const workspaceKey = decryptAsymmetric({
ciphertext: projectV1Keys.projectEncryptedKey,
nonce: projectV1Keys.projectKeyNonce,
publicKey: projectV1Keys.senderPublicKey,
privateKey: userPrivateKey
});
const botKey = generateAsymmetricKeyPair();
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(botKey.privateKey);
const encryptedWorkspaceKey = encryptAsymmetric(workspaceKey, botKey.publicKey, userPrivateKey);
if (!bot) {
await projectBotDAL.create({
name: "Infisical Bot (Ghost)",
projectId,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: botKey.publicKey,
algorithm,
keyEncoding: encoding,
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
senderId: projectV1Keys.userId
});
} else {
await projectBotDAL.updateById(bot.id, {
isActive: true,
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
senderId: projectV1Keys.userId
});
}
return { botKey: workspaceKey, project, shouldUseSecretV2Bridge: false };
}
const botPrivateKey = getBotPrivateKey({ bot }); const botPrivateKey = getBotPrivateKey({ bot });
return decryptAsymmetric({ const botKey = decryptAsymmetric({
ciphertext: bot.encryptedProjectKey, ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey, privateKey: botPrivateKey,
nonce: bot.encryptedProjectKeyNonce, nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey publicKey: bot.sender.publicKey
}); });
return { botKey, project, shouldUseSecretV2Bridge: false };
}; };
return getBotKeyFn; return getBotKeyFn;
@@ -60,7 +60,7 @@ export const projectBotServiceFactory = ({
const project = await projectDAL.findById(projectId, tx); const project = await projectDAL.findById(projectId, tx);
if (project.version === ProjectVersion.V2) { if (project.version === ProjectVersion.V2 || project.version === ProjectVersion.V3) {
throw new BadRequestError({ message: "Failed to create bot, project is upgraded." }); throw new BadRequestError({ message: "Failed to create bot, project is upgraded." });
} }
+148 -48
View File
@@ -8,8 +8,6 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -21,14 +19,13 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
@@ -38,11 +35,14 @@ import {
TCreateProjectDTO, TCreateProjectDTO,
TDeleteProjectDTO, TDeleteProjectDTO,
TGetProjectDTO, TGetProjectDTO,
TGetProjectKmsKey,
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertsDTO, TListProjectCertsDTO,
TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO, TUpdateAuditLogsRetentionDTO,
TUpdateProjectDTO, TUpdateProjectDTO,
TUpdateProjectKmsDTO,
TUpdateProjectNameDTO, TUpdateProjectNameDTO,
TUpdateProjectVersionLimitDTO, TUpdateProjectVersionLimitDTO,
TUpgradeProjectDTO TUpgradeProjectDTO
@@ -65,10 +65,8 @@ type TProjectServiceFactoryDep = {
identityProjectDAL: TIdentityProjectDALFactory; identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">; identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">; certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
@@ -76,6 +74,15 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
| "getProjectKeyBackup"
| "loadProjectKeyBackup"
| "getKmsById"
| "getProjectSecretManagerKmsKeyId"
| "deleteInternalKms"
>;
}; };
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>; export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -90,9 +97,7 @@ export const projectServiceFactory = ({
folderDAL, folderDAL,
orgService, orgService,
identityProjectDAL, identityProjectDAL,
projectBotDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
secretBlindIndexDAL,
projectMembershipDAL, projectMembershipDAL,
projectEnvDAL, projectEnvDAL,
licenseService, licenseService,
@@ -100,7 +105,8 @@ export const projectServiceFactory = ({
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
keyStore keyStore,
kmsService
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * Create workspace. Make user the admin
@@ -111,7 +117,8 @@ export const projectServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
workspaceName, workspaceName,
slug: projectSlug slug: projectSlug,
kmsKeyId
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -124,9 +131,6 @@ export const projectServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
const appCfg = getConfig();
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
const plan = await licenseService.getPlan(organization.id); const plan = await licenseService.getPlan(organization.id);
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) { if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
// case: limit imposed on number of workspaces allowed // case: limit imposed on number of workspaces allowed
@@ -139,16 +143,28 @@ export const projectServiceFactory = ({
const results = await projectDAL.transaction(async (tx) => { const results = await projectDAL.transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(organization.id, tx); const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) {
const kms = await kmsService.getKmsById(kmsKeyId, tx);
if (kms.orgId !== organization.id) {
throw new BadRequestError({
message: "KMS does not belong in the organization"
});
}
}
const project = await projectDAL.create( const project = await projectDAL.create(
{ {
name: workspaceName, name: workspaceName,
orgId: organization.id, orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
version: ProjectVersion.V2, kmsSecretManagerKeyId: kmsKeyId,
version: ProjectVersion.V3,
pitVersionLimit: 10 pitVersionLimit: 10
}, },
tx tx
); );
// set ghost user as admin of project // set ghost user as admin of project
const projectMembership = await projectMembershipDAL.create( const projectMembership = await projectMembershipDAL.create(
{ {
@@ -162,18 +178,6 @@ export const projectServiceFactory = ({
tx tx
); );
// generate the blind index for project
await secretBlindIndexDAL.create(
{
projectId: project.id,
keyEncoding: blindIndex.keyEncoding,
saltIV: blindIndex.iv,
saltTag: blindIndex.tag,
algorithm: blindIndex.algorithm,
encryptedSaltCipherText: blindIndex.ciphertext
},
tx
);
// set default environments and root folder for provided environments // set default environments and root folder for provided environments
const envs = await projectEnvDAL.insertMany( const envs = await projectEnvDAL.insertMany(
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })), DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
@@ -202,26 +206,7 @@ export const projectServiceFactory = ({
tx tx
); );
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey); // const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
// 5. Create & a bot for the project
await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
// Find the ghost users latest key // Find the ghost users latest key
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx); const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
@@ -353,7 +338,12 @@ export const projectServiceFactory = ({
const deletedProject = await projectDAL.transaction(async (tx) => { const deletedProject = await projectDAL.transaction(async (tx) => {
const delProject = await projectDAL.deleteById(project.id, tx); const delProject = await projectDAL.deleteById(project.id, tx);
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null); const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null);
if (delProject.kmsCertificateKeyId) {
await kmsService.deleteInternalKms(delProject.kmsCertificateKeyId, delProject.orgId, tx);
}
if (delProject.kmsSecretManagerKeyId) {
await kmsService.deleteInternalKms(delProject.kmsSecretManagerKeyId, delProject.orgId, tx);
}
// Delete the org membership for the ghost user if it's found. // Delete the org membership for the ghost user if it's found.
if (projectGhostUser) { if (projectGhostUser) {
await userDAL.deleteById(projectGhostUser.id, tx); await userDAL.deleteById(projectGhostUser.id, tx);
@@ -664,6 +654,112 @@ export const projectServiceFactory = ({
}; };
}; };
const updateProjectKmsKey = async ({
projectId,
kms,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TUpdateProjectKmsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey({
projectId,
kms
});
return {
secretManagerKmsKey
};
};
const getProjectKmsBackup = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to get KMS backup due to plan restriction. Upgrade to the enterprise plan."
});
}
const kmsBackup = await kmsService.getProjectKeyBackup(projectId);
return kmsBackup;
};
const loadProjectKmsBackup = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
backup
}: TLoadProjectKmsBackupDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to load KMS backup due to plan restriction. Upgrade to the enterprise plan."
});
}
const kmsBackup = await kmsService.loadProjectKeyBackup(projectId, backup);
return kmsBackup;
};
const getProjectKmsKeys = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TGetProjectKmsKey) => {
const { membership } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
if (!membership) {
throw new ForbiddenRequestError({
message: "User is not a member of the project"
});
}
const kmsKeyId = await kmsService.getProjectSecretManagerKmsKeyId(projectId);
const kmsKey = await kmsService.getKmsById(kmsKeyId);
return { secretManagerKmsKey: kmsKey };
};
return { return {
createProject, createProject,
deleteProject, deleteProject,
@@ -677,6 +773,10 @@ export const projectServiceFactory = ({
listProjectCas, listProjectCas,
listProjectCertificates, listProjectCertificates,
updateVersionLimit, updateVersionLimit,
updateAuditLogsRetention updateAuditLogsRetention,
updateProjectKmsKey,
getProjectKmsBackup,
loadProjectKmsBackup,
getProjectKmsKeys
}; };
}; };
@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { CaStatus } from "../certificate-authority/certificate-authority-types"; import { CaStatus } from "../certificate-authority/certificate-authority-types";
import { KmsType } from "../kms/kms-types";
export enum ProjectFilterType { export enum ProjectFilterType {
ID = "id", ID = "id",
@@ -27,6 +28,7 @@ export type TCreateProjectDTO = {
actorOrgId?: string; actorOrgId?: string;
workspaceName: string; workspaceName: string;
slug?: string; slug?: string;
kmsKeyId?: string;
}; };
export type TDeleteProjectBySlugDTO = { export type TDeleteProjectBySlugDTO = {
@@ -103,3 +105,13 @@ export type TListProjectCertsDTO = {
friendlyName?: string; friendlyName?: string;
commonName?: string; commonName?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateProjectKmsDTO = {
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
} & TProjectPermission;
export type TLoadProjectKmsBackupDTO = {
backup: string;
} & TProjectPermission;
export type TGetProjectKmsKey = TProjectPermission;
@@ -7,11 +7,13 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
type TDailyResourceCleanUpQueueServiceFactoryDep = { type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">; auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">; secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">; secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">; snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">; secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
@@ -27,7 +29,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretFolderVersionDAL, secretFolderVersionDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL secretSharingDAL,
secretVersionV2DAL
}: TDailyResourceCleanUpQueueServiceFactoryDep) => { }: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => { queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
@@ -36,6 +39,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretSharingDAL.pruneExpiredSharedSecrets(); await secretSharingDAL.pruneExpiredSharedSecrets();
await snapshotDAL.pruneExcessSnapshots(); await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });
@@ -312,12 +312,14 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const folder = await (tx || db.replicaNode())(TableName.SecretFolder) const folder = await (tx || db.replicaNode())(TableName.SecretFolder)
.where({ [`${TableName.SecretFolder}.id` as "id"]: id }) .where({ [`${TableName.SecretFolder}.id` as "id"]: id })
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.SecretFolder)) .select(selectAllTableCols(TableName.SecretFolder))
.select( .select(
db.ref("id").withSchema(TableName.Environment).as("envId"), db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName"), db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("projectId").withSchema(TableName.Environment) db.ref("projectId").withSchema(TableName.Environment),
db.ref("version").withSchema(TableName.Project).as("projectVersion")
) )
.first(); .first();
if (folder) { if (folder) {
@@ -329,6 +331,27 @@ export const secretFolderDALFactory = (db: TDbClient) => {
} }
}; };
// special query for project migration
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.SecretFolder))
.where({ projectId })
.select(
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("projectId").withSchema(TableName.Environment),
db.ref("version").withSchema(TableName.Project).as("projectVersion")
);
return folders;
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
};
return { return {
...secretFolderOrm, ...secretFolderOrm,
update, update,
@@ -336,6 +359,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
findById, findById,
findByManySecretPath, findByManySecretPath,
findSecretPathByFolderIds, findSecretPathByFolderIds,
findClosestFolder findClosestFolder,
findByProjectId
}; };
}; };
@@ -1,8 +1,9 @@
import { SecretType, TSecretImports, TSecrets } from "@app/db/schemas"; import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas";
import { groupBy } from "@app/lib/fn"; import { groupBy, unique } from "@app/lib/fn";
import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretImportDALFactory } from "./secret-import-dal"; import { TSecretImportDALFactory } from "./secret-import-dal";
type TSecretImportSecrets = { type TSecretImportSecrets = {
@@ -18,6 +19,28 @@ type TSecretImportSecrets = {
secrets: (TSecrets & { workspace: string; environment: string; _id: string })[]; secrets: (TSecrets & { workspace: string; environment: string; _id: string })[];
}; };
type TSecretImportSecretsV2 = {
secretPath: string;
environment: string;
environmentInfo: {
id: string;
slug: string;
name: string;
};
folderId: string | undefined;
importFolderId: string;
secrets: (TSecretsV2 & {
workspace: string;
environment: string;
_id: string;
secretKey: string;
// akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string | undefined;
secretComment: string | undefined;
})[];
};
const LEVEL_BREAK = 10; const LEVEL_BREAK = 10;
const getImportUniqKey = (envSlug: string, path: string) => `${envSlug}=${path}`; const getImportUniqKey = (envSlug: string, path: string) => `${envSlug}=${path}`;
export const fnSecretsFromImports = async ({ export const fnSecretsFromImports = async ({
@@ -115,3 +138,133 @@ export const fnSecretsFromImports = async ({
return secrets; return secrets;
}; };
export const fnSecretsV2FromImports = async ({
allowedImports: possibleCyclicImports,
folderDAL,
secretDAL,
secretImportDAL,
depth = 0,
cyclicDetector = new Set(),
decryptor,
expandSecretReferences
}: {
allowedImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string };
})[];
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
depth?: number;
cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string | undefined;
expandSecretReferences?: (
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
}) => {
// avoid going more than a depth
if (depth >= LEVEL_BREAK) return [];
const allowedImports = possibleCyclicImports.filter(
({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath))
);
const importedFolders = (
await folderDAL.findByManySecretPath(
allowedImports.map(({ importEnv, importPath }) => ({
envId: importEnv.id,
secretPath: importPath
}))
)
).filter(Boolean); // remove undefined ones
if (!importedFolders.length) {
return [];
}
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
const importedSecrets = await secretDAL.find(
{
$in: { folderId: importedFolderIds },
type: SecretType.Shared
},
{
sort: [["id", "asc"]]
}
);
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
allowedImports.forEach(({ importPath, importEnv }) => {
cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath));
});
// now we need to check recursively deeper imports made inside other imports
// we go level wise meaning we take all imports of a tree level and then go deeper ones level by level
const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds);
let secretsFromDeeperImports: TSecretImportSecretsV2[] = [];
if (deeperImports.length) {
secretsFromDeeperImports = await fnSecretsV2FromImports({
allowedImports: deeperImports.filter(({ isReplication }) => !isReplication),
secretImportDAL,
folderDAL,
secretDAL,
depth: depth + 1,
cyclicDetector,
decryptor,
expandSecretReferences
});
}
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0];
const folderDeeperImportSecrets =
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
.map((item) => ({
...item,
secretKey: item.key,
secretValue: decryptor(item.encryptedValue),
secretComment: decryptor(item.encryptedComment),
environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
}))
.concat(folderDeeperImportSecrets);
return {
secretPath: importPath,
environment: importEnv.slug,
environmentInfo: importEnv,
folderId: importedFolders?.[i]?.id,
id,
importFolderId: folderId,
secrets: unique(secretsWithDuplicate, (el) => el.secretKey)
};
});
if (expandSecretReferences) {
await Promise.all(
processedImports.map(async (processedImport) => {
const secretsGroupByKey = processedImport.secrets.reduce(
(acc, item) => {
acc[item.secretKey] = {
value: item.secretValue,
comment: item.secretComment,
skipMultilineEncoding: item.skipMultilineEncoding
};
return acc;
},
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey);
processedImport.secrets.forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
});
})
);
}
return processedImports;
};
@@ -9,13 +9,18 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretDALFactory } from "../secret/secret-dal";
import { decryptSecretRaw } from "../secret/secret-fns";
import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretImportDALFactory } from "./secret-import-dal"; import { TSecretImportDALFactory } from "./secret-import-dal";
import { fnSecretsFromImports } from "./secret-import-fns"; import { fnSecretsFromImports, fnSecretsV2FromImports } from "./secret-import-fns";
import { import {
TCreateSecretImportDTO, TCreateSecretImportDTO,
TDeleteSecretImportDTO, TDeleteSecretImportDTO,
@@ -29,11 +34,14 @@ type TSecretImportServiceFactoryDep = {
secretImportDAL: TSecretImportDALFactory; secretImportDAL: TSecretImportDALFactory;
folderDAL: TSecretFolderDALFactory; folderDAL: TSecretFolderDALFactory;
secretDAL: Pick<TSecretDALFactory, "find">; secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectEnvDAL: TProjectEnvDALFactory; projectEnvDAL: TProjectEnvDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" }); const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
@@ -48,7 +56,10 @@ export const secretImportServiceFactory = ({
projectDAL, projectDAL,
secretDAL, secretDAL,
secretQueueService, secretQueueService,
licenseService licenseService,
projectBotService,
secretV2BridgeDAL,
kmsService
}: TSecretImportServiceFactoryDep) => { }: TSecretImportServiceFactoryDep) => {
const createImport = async ({ const createImport = async ({
environment, environment,
@@ -449,12 +460,76 @@ export const secretImportServiceFactory = ({
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
}; };
const getRawSecretsFromImports = async ({
path: secretPath,
environment,
projectId,
actor,
actorAuthMethod,
actorId,
actorOrgId
}: TGetSecretsFromImportDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return [];
// this will already order by position
// so anything based on this order will also be in right position
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug,
secretPath: importPath
})
)
);
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const importedSecrets = await fnSecretsV2FromImports({
allowedImports,
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
});
return importedSecrets;
}
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
return importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((encryptedSecret) =>
decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey)
)
}));
};
return { return {
createImport, createImport,
updateImport, updateImport,
deleteImport, deleteImport,
getImports, getImports,
getSecretsFromImports, getSecretsFromImports,
getRawSecretsFromImports,
resyncSecretImportReplication, resyncSecretImportReplication,
fnSecretsFromImports fnSecretsFromImports
}; };
@@ -3,13 +3,14 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretTagDALFactory = ReturnType<typeof secretTagDALFactory>; export type TSecretTagDALFactory = ReturnType<typeof secretTagDALFactory>;
export const secretTagDALFactory = (db: TDbClient) => { export const secretTagDALFactory = (db: TDbClient) => {
const secretTagOrm = ormify(db, TableName.SecretTag); const secretTagOrm = ormify(db, TableName.SecretTag);
const secretJnTagOrm = ormify(db, TableName.JnSecretTag); const secretJnTagOrm = ormify(db, TableName.JnSecretTag);
const secretV2JnTagOrm = ormify(db, TableName.SecretV2JnTag);
const findManyTagsById = async (projectId: string, ids: string[], tx?: Knex) => { const findManyTagsById = async (projectId: string, ids: string[], tx?: Knex) => {
try { try {
@@ -34,10 +35,25 @@ export const secretTagDALFactory = (db: TDbClient) => {
} }
}; };
// special query for migration
const findSecretTagsByProjectId = async (projectId: string, tx?: Knex) => {
try {
const tags = await (tx || db.replicaNode())(TableName.JnSecretTag)
.join(TableName.SecretTag, `${TableName.JnSecretTag}.secret_tagsId`, `${TableName.SecretTag}.id`)
.where({ projectId })
.select(selectAllTableCols(TableName.JnSecretTag));
return tags;
} catch (error) {
throw new DatabaseError({ error, name: "Find all by ids" });
}
};
return { return {
...secretTagOrm, ...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany, saveTagsToSecret: secretJnTagOrm.insertMany,
deleteTagsToSecret: secretJnTagOrm.delete, deleteTagsToSecret: secretJnTagOrm.delete,
saveTagsToSecretV2: secretV2JnTagOrm.insertMany,
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
findSecretTagsByProjectId,
deleteTagsManySecret, deleteTagsManySecret,
findManyTagsById findManyTagsById
}; };
@@ -0,0 +1,393 @@
import { Knex } from "knex";
import { validate as uuidValidate } from "uuid";
import { TDbClient } from "@app/db";
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
export const secretV2BridgeDALFactory = (db: TDbClient) => {
const secretOrm = ormify(db, TableName.SecretV2);
const update = async (filter: Partial<TSecretsV2>, data: Omit<TSecretsV2Update, "version">, tx?: Knex) => {
try {
const sec = await (tx || db)(TableName.SecretV2)
.where(filter)
.update(data)
.increment("version", 1)
.returning("*");
return sec;
} catch (error) {
throw new DatabaseError({ error, name: "update secret" });
}
};
const bulkUpdate = async (
data: Array<{ filter: Partial<TSecretsV2>; data: TSecretsV2Update }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.SecretV2)
.where(filter)
.update(updateData)
.increment("version", 1)
.returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const bulkUpdateNoVersionIncrement = async (data: TSecretsV2[], tx?: Knex) => {
try {
const existingSecrets = await secretOrm.find(
{
$in: {
id: data.map((el) => el.id)
}
},
{ tx }
);
if (existingSecrets.length !== data.length) {
throw new BadRequestError({ message: "Some of the secrets do not exist" });
}
if (data.length === 0) return [];
const updatedSecrets = await (tx || db)(TableName.SecretV2)
.insert(data)
.onConflict("id") // this will cause a conflict then merge the data
.merge() // Merge the data with the existing data
.returning("*");
return updatedSecrets;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const deleteMany = async (
data: Array<{ key: string; type: SecretType }>,
folderId: string,
userId: string,
tx?: Knex
) => {
try {
const deletedSecrets = await (tx || db)(TableName.SecretV2)
.where({ folderId })
.where((bd) => {
data.forEach((el) => {
void bd.orWhere({
key: el.key,
type: el.type,
...(el.type === SecretType.Personal ? { userId } : {})
});
// if shared is getting deleted then personal ones also should be deleted
if (el.type === SecretType.Shared) {
void bd.orWhere({
key: el.key,
type: SecretType.Personal
});
}
});
})
.delete()
.returning("*");
return deletedSecrets;
} catch (error) {
throw new DatabaseError({ error, name: "delete many secret" });
}
};
const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => {
try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) {
// eslint-disable-next-line
userId = undefined;
}
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
.where({ folderId })
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
})
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
id,
color,
slug,
name
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get all secret" });
}
};
const getSecretTags = async (secretId: string, tx?: Knex) => {
try {
const tags = await (tx || db.replicaNode())(TableName.SecretV2JnTag)
.join(TableName.SecretTag, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.where({ [`${TableName.SecretV2}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({
id: el.tagId,
color: el.tagColor,
slug: el.tagSlug,
name: el.tagName
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret tags" });
}
};
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) {
// eslint-disable-next-line no-param-reassign
userId = undefined;
}
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
.whereIn("folderId", folderIds)
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
})
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc");
const data = sqlNestRelationships({
data: secs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
id,
color,
slug,
name
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get all secret" });
}
};
const findBySecretKeys = async (
folderId: string,
query: Array<{ key: string; type: SecretType.Shared } | { key: string; type: SecretType.Personal; userId: string }>,
tx?: Knex
) => {
if (!query.length) return [];
try {
const secrets = await (tx || db.replicaNode())(TableName.SecretV2)
.where({ folderId })
.where((bd) => {
query.forEach((el) => {
if (el.type === SecretType.Personal && !el.userId) {
throw new BadRequestError({ message: "Missing personal user id" });
}
void bd.orWhere({
key: el.key,
type: el.type,
userId: el.type === SecretType.Personal ? el.userId : null
});
});
});
return secrets;
} catch (error) {
throw new DatabaseError({ error, name: "find by blind indexes" });
}
};
const upsertSecretReferences = async (
data: {
secretId: string;
references: Array<{ environment: string; secretPath: string; secretKey: string }>;
}[] = [],
tx?: Knex
) => {
try {
if (!data.length) return;
await (tx || db)(TableName.SecretReferenceV2)
.whereIn(
"secretId",
data.map(({ secretId }) => secretId)
)
.delete();
const newSecretReferences = data
.filter(({ references }) => references.length)
.flatMap(({ secretId, references }) =>
references.map(({ environment, secretPath, secretKey }) => ({
secretPath,
secretId,
environment,
secretKey
}))
);
if (!newSecretReferences.length) return;
const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences);
return secretReferences;
} catch (error) {
throw new DatabaseError({ error, name: "UpsertSecretReference" });
}
};
const findReferencedSecretReferences = async (projectId: string, envSlug: string, secretPath: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretReferenceV2)
.where({
secretPath,
environment: envSlug
})
.join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretReferenceV2}.secretId`)
.join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where("projectId", projectId)
.select(selectAllTableCols(TableName.SecretReferenceV2))
.select("folderId");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindReferencedSecretReferences" });
}
};
// special query to backfill secret value
const findAllProjectSecretValues = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretV2)
.join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where("projectId", projectId)
// not empty
.whereNotNull("encryptedValue")
.select("encryptedValue", `${TableName.SecretV2}.id` as "id");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindAllProjectSecretValues" });
}
};
const findOneWithTags = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
try {
const rawDocs = await (tx || db.replicaNode())(TableName.SecretV2)
.where(filter)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
id,
color,
slug,
name
})
}
]
});
return docs?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindOneWIthTags" });
}
};
return {
...secretOrm,
update,
bulkUpdate,
deleteMany,
bulkUpdateNoVersionIncrement,
getSecretTags,
findOneWithTags,
findByFolderId,
findByFolderIds,
findBySecretKeys,
upsertSecretReferences,
findReferencedSecretReferences,
findAllProjectSecretValues
};
};
@@ -0,0 +1,559 @@
import path from "node:path";
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
export const shouldUseSecretV2Bridge = (version: number) => version === 3;
/**
* Grabs and processes nested secret references from a string
*
* This function looks for patterns that match the interpolation syntax in the input string.
* It filters out references that include nested paths, splits them into environment and
* secret path parts, and then returns an array of objects with the environment and the
* joined secret path.
* @example
* const value = "Hello ${dev.someFolder.OtherFolder.SECRET_NAME} and ${prod.anotherFolder.SECRET_NAME}";
* const result = getAllNestedSecretReferences(value);
* // result will be:
* // [
* // { environment: 'dev', secretPath: '/someFolder/OtherFolder' },
* // { environment: 'prod', secretPath: '/anotherFolder' }
* // ]
*/
export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
return references
.filter((el) => el.includes("."))
.map((el) => {
const [environment, ...secretPathList] = el.split(".");
return {
environment,
secretPath: path.join("/", ...secretPathList.slice(0, -1)),
secretKey: secretPathList[secretPathList.length - 1]
};
});
};
// these functions are special functions shared by a couple of resources
// used by secret approval, rotation or anywhere in which secret needs to modified
export const fnSecretBulkInsert = async ({
// TODO: Pick types here
folderId,
inputSecrets,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
}: TFnSecretBulkInsert) => {
const sanitizedInputSecrets = inputSecrets.map(
({
skipMultilineEncoding,
type,
key,
userId,
encryptedComment,
version,
metadata,
reminderNote,
encryptedValue,
reminderRepeatDays
}) => ({
skipMultilineEncoding,
type,
key,
userId,
encryptedComment,
version,
metadata,
reminderNote,
encryptedValue,
reminderRepeatDays
})
);
const newSecrets = await secretDAL.insertMany(sanitizedInputSecrets.map((el) => ({ ...el, folderId })));
const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key);
const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) =>
secretTags.map((tag) => ({
[`${TableName.SecretTag}Id` as const]: tag,
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
}))
);
const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({
...el,
folderId,
secretId: newSecretGroupedByKeyName[el.key][0].id
})),
tx
);
await secretDAL.upsertSecretReferences(
inputSecrets.map(({ references = [], key }) => ({
secretId: newSecretGroupedByKeyName[key][0].id,
references
})),
tx
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
export const fnSecretBulkUpdate = async ({
tx,
inputSecrets,
folderId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
}: TFnSecretBulkUpdate) => {
const sanitizedInputSecrets = inputSecrets.map(
({
filter,
data: {
skipMultilineEncoding,
type,
key,
encryptedValue,
userId,
encryptedComment,
metadata,
reminderNote,
reminderRepeatDays
}
}) => ({
filter: { ...filter, folderId },
data: {
skipMultilineEncoding,
type,
key,
userId,
encryptedComment,
metadata,
reminderNote,
encryptedValue,
reminderRepeatDays
}
})
);
const newSecrets = await secretDAL.bulkUpdate(sanitizedInputSecrets, tx);
const secretVersions = await secretVersionDAL.insertMany(
newSecrets.map(
({
skipMultilineEncoding,
type,
key,
userId,
encryptedComment,
version,
metadata,
reminderNote,
encryptedValue,
reminderRepeatDays,
id: secretId
}) => ({
skipMultilineEncoding,
type,
key,
userId,
encryptedComment,
version,
metadata,
reminderNote,
encryptedValue,
reminderRepeatDays,
folderId,
secretId
})
),
tx
);
await secretDAL.upsertSecretReferences(
inputSecrets
.filter(({ data: { references } }) => Boolean(references))
.map(({ data: { references = [] } }, i) => ({
secretId: newSecrets[i].id,
references
})),
tx
);
const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) =>
tags !== undefined ? { tags, secretId: newSecrets[i].id } : []
);
if (secsUpdatedTag.length) {
await secretTagDAL.deleteTagsToSecretV2(
{ $in: { secrets_v2Id: secsUpdatedTag.map(({ secretId }) => secretId) } },
tx
);
const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], secretId }) =>
secretTags.map((tag) => ({
[`${TableName.SecretTag}Id` as const]: tag,
[`${TableName.SecretV2}Id` as const]: secretId
}))
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
}
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
export const fnSecretBulkDelete = async ({
folderId,
inputSecrets,
tx,
actorId,
secretDAL,
secretQueueService
}: TFnSecretBulkDelete) => {
const deletedSecrets = await secretDAL.deleteMany(
inputSecrets.map(({ type, secretKey }) => ({
key: secretKey,
type
})),
folderId,
actorId,
tx
);
await Promise.allSettled(
deletedSecrets
.filter(({ reminderRepeatDays }) => Boolean(reminderRepeatDays))
.map(({ id, reminderRepeatDays }) =>
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: reminderRepeatDays as number })
)
);
return deletedSecrets;
};
// Introduce a new interface for mapping parent IDs to their children
interface FolderMap {
[parentId: string]: TSecretFolders[];
}
const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
const map: FolderMap = {};
map.null = []; // Initialize mapping for root directory
folders.forEach((folder) => {
const parentId = folder.parentId || "null";
if (!map[parentId]) {
map[parentId] = [];
}
map[parentId].push(folder);
});
return map;
};
const generatePaths = (
map: FolderMap,
parentId: string = "null",
basePath: string = "",
currentDepth: number = 0
): { path: string; folderId: string }[] => {
const children = map[parentId || "null"] || [];
let paths: { path: string; folderId: string }[] = [];
children.forEach((child) => {
// Determine if this is the root folder of the environment. If no parentId is present and the name is root, it's the root folder
const isRootFolder = child.name === "root" && !child.parentId;
// Form the current path based on the base path and the current child
// eslint-disable-next-line no-nested-ternary
const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
// Add the current path
paths.push({
path: currPath,
folderId: child.id
});
// We make sure that the recursion depth doesn't exceed 20.
// We do this to create "circuit break", basically to ensure that we can't encounter any potential memory leaks.
if (currentDepth >= 20) {
logger.info(`generatePaths: Recursion depth exceeded 20, breaking out of recursion [map=${JSON.stringify(map)}]`);
return;
}
// Recursively generate paths for children, passing down the formatted path
const childPaths = generatePaths(map, child.id, currPath, currentDepth + 1);
paths = paths.concat(
childPaths.map((p) => ({
path: p.path,
folderId: p.folderId
}))
);
});
return paths;
};
type TRecursivelyFetchSecretsFromFoldersArg = {
folderDAL: Pick<TSecretFolderDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectId: string;
environment: string;
currentPath: string;
hasAccess: (environment: string, secretPath: string) => boolean;
};
export const recursivelyGetSecretPaths = async ({
folderDAL,
projectEnvDAL,
projectId,
environment,
currentPath,
hasAccess
}: TRecursivelyFetchSecretsFromFoldersArg) => {
const env = await projectEnvDAL.findOne({
projectId,
slug: environment
});
if (!env) {
throw new Error(`'${environment}' environment not found in project with ID ${projectId}`);
}
// Fetch all folders in env once with a single query
const folders = await folderDAL.find({
envId: env.id,
isReserved: false
});
// Build the folder hierarchy map
const folderMap = buildHierarchy(folders);
// Generate the paths paths and normalize the root path to /
const paths = generatePaths(folderMap).map((p) => ({
path: p.path === "/" ? p.path : p.path.substring(1),
folderId: p.folderId
}));
// Filter out paths that the user does not have permission to access, and paths that are not in the current path
const allowedPaths = paths.filter(
(folder) => hasAccess(environment, folder.path) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
);
return allowedPaths;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
type TInterpolateSecretArg = {
projectId: string;
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
export const expandSecretReferencesFactory = ({
projectId,
decryptSecretValue: decryptSecret,
secretDAL,
folderDAL
}: TInterpolateSecretArg) => {
const fetchSecretFactory = () => {
const secretCache: Record<string, Record<string, string>> = {};
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const referredSecretPathURL = path.join("/", ...secRefPath);
const uniqueKey = `${secRefEnv}-${referredSecretPathURL}`;
if (secretCache?.[uniqueKey]) {
return secretCache[uniqueKey][secRefKey];
}
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, referredSecretPathURL);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => {
// eslint-disable-next-line
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
return prev;
}, {});
secretCache[uniqueKey] = decryptedSecret;
return secretCache[uniqueKey][secRefKey];
};
};
const recursivelyExpandSecret = async (
expandedSec: Record<string, string | undefined>,
interpolatedSec: Record<string, string | undefined>,
fetchSecret: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
): Promise<string | undefined> => {
if (expandedSec?.[key] !== undefined) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
// eslint-disable-next-line
recursionChainBreaker[key] = true;
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length === 1) {
// eslint-disable-next-line
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchSecret,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
// eslint-disable-next-line
continue;
}
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
// eslint-disable-next-line
const val = await fetchSecret(secRefEnv, secRefPath, secRefKey);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
}
}
}
// eslint-disable-next-line
expandedSec[key] = interpolatedValue;
return interpolatedValue;
};
const fetchSecret = fetchSecretFactory();
const expandSecrets = async (
inputSecrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSecrets: Record<string, string | undefined> = {};
const toBeExpandedSecrets: Record<string, string | undefined> = {};
Object.keys(inputSecrets).forEach((key) => {
if (inputSecrets[key].value?.match(INTERPOLATION_SYNTAX_REG)) {
toBeExpandedSecrets[key] = inputSecrets[key].value;
} else {
expandedSecrets[key] = inputSecrets[key].value;
}
});
for (const key of Object.keys(inputSecrets)) {
if (expandedSecrets?.[key]) {
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedSecrets[key])
: expandedSecrets[key];
// eslint-disable-next-line
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
// eslint-disable-next-line
const expandedVal = await recursivelyExpandSecret(
expandedSecrets,
toBeExpandedSecrets,
fetchSecret,
recursionChainBreaker,
key
);
// eslint-disable-next-line
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedVal)
: expandedVal;
}
return inputSecrets;
};
return expandSecrets;
};
export const reshapeBridgeSecret = (
workspaceId: string,
environment: string,
secretPath: string,
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value?: string;
comment?: string;
tags?: {
id: string;
slug: string;
color?: string | null;
name: string;
}[];
}
) => ({
secretKey: secret.key,
secretPath,
workspace: workspaceId,
environment,
secretValue: secret.value,
secretComment: secret.comment,
version: secret.version,
type: secret.type,
_id: secret.id,
id: secret.id,
user: secret.userId,
tags: secret.tags,
skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderNote: secret.reminderNote,
metadata: secret.metadata,
createdAt: secret.createdAt,
updatedAt: secret.updatedAt
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,267 @@
import { Knex } from "knex";
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "./secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal";
type TPartialSecret = Pick<TSecretsV2, "id" | "reminderRepeatDays" | "reminderNote">;
type TPartialInputSecret = Pick<TSecretsV2, "type" | "reminderNote" | "reminderRepeatDays" | "id">;
export type TGetSecretsDTO = {
expandSecretReferences?: boolean;
path: string;
environment: string;
includeImports?: boolean;
recursive?: boolean;
} & TProjectPermission;
export type TGetASecretDTO = {
secretName: string;
path: string;
environment: string;
expandSecretReferences?: boolean;
type: "shared" | "personal";
includeImports?: boolean;
version?: number;
projectId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateSecretDTO = TProjectPermission & {
secretName: string;
secretPath: string;
environment: string;
secretValue: string;
type: SecretType;
tagIds?: string[];
secretComment?: string;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
};
export type TUpdateSecretDTO = TProjectPermission & {
secretPath: string;
environment: string;
secretName: string;
secretValue?: string;
newSecretName?: string;
secretComment?: string;
type: SecretType;
tagIds?: string[];
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
metadata?: {
source?: string;
};
};
export type TDeleteSecretDTO = TProjectPermission & {
secretPath: string;
environment: string;
secretName: string;
type: SecretType;
};
export type TCreateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectId: string;
environment: string;
secrets: {
secretKey: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
metadata?: {
source?: string;
};
}[];
};
export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectId: string;
environment: string;
secrets: {
secretKey: string;
newSecretName?: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
}[];
};
export type TDeleteManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectId: string;
environment: string;
secrets: {
secretKey: string;
type?: SecretType;
}[];
};
export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
limit?: number;
offset?: number;
secretId: string;
};
export type TSecretReference = { environment: string; secretPath: string; secretKey: string };
export type TFnSecretBulkInsert = {
folderId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsV2Insert, "folderId"> & { tagIds?: string[]; references: TSecretReference[] }>;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
};
type TRequireReferenceIfValue =
| (Omit<TSecretsV2Update, "encryptedValue"> & {
encryptedValue: Buffer | null;
references: TSecretReference[];
})
| (Omit<TSecretsV2Update, "encryptedValue"> & {
encryptedValue?: never;
references?: never;
});
export type TFnSecretBulkUpdate = {
folderId: string;
inputSecrets: {
filter: Partial<TSecretsV2>;
data: TRequireReferenceIfValue & { tags?: string[] };
}[];
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
tx?: Knex;
};
export type TFnSecretBulkDelete = {
folderId: string;
projectId: string;
inputSecrets: Array<{ type: SecretType; secretKey: string }>;
actorId: string;
tx?: Knex;
secretDAL: Pick<TSecretV2BridgeDALFactory, "deleteMany">;
secretQueueService: {
removeSecretReminder: (data: TRemoveSecretReminderDTO) => Promise<void>;
};
};
export type THandleReminderDTO = {
newSecret: TPartialInputSecret;
oldSecret: TPartialSecret;
projectId: string;
};
export type TCreateSecretReminderDTO = {
oldSecret: TPartialSecret;
newSecret: TPartialSecret;
projectId: string;
};
export type TRemoveSecretReminderDTO = {
secretId: string;
repeatDays: number;
};
export type TBackFillSecretReferencesDTO = TProjectPermission;
export type TCreateManySecretsFnFactory = {
projectDAL: TProjectDALFactory;
secretDAL: TSecretV2BridgeDALFactory;
secretVersionDAL: TSecretVersionV2DALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionV2TagDALFactory;
folderDAL: TSecretFolderDALFactory;
};
export type TCreateManySecretsFn = {
projectId: string;
environment: string;
path: string;
secrets: {
secretName: string;
secretValue: string;
type: SecretType;
secretComment?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
metadata?: {
source?: string;
};
}[];
userId?: string; // only relevant for personal secret(s)
};
export type TUpdateManySecretsFnFactory = {
projectDAL: TProjectDALFactory;
secretDAL: TSecretV2BridgeDALFactory;
secretVersionDAL: TSecretVersionV2DALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionV2TagDALFactory;
folderDAL: TSecretFolderDALFactory;
};
export type TUpdateManySecretsFn = {
projectId: string;
environment: string;
path: string;
secrets: {
secretName: string;
newSecretName?: string;
secretValue: string;
type: SecretType;
secretComment?: string;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
tags?: string[];
metadata?: {
source?: string;
};
}[];
userId?: string;
};
export enum SecretOperations {
Create = "create",
Update = "update",
Delete = "delete"
}
export type TMoveSecretsDTO = {
projectId: string;
sourceEnvironment: string;
sourceSecretPath: string;
destinationEnvironment: string;
destinationSecretPath: string;
secretIds: string[];
shouldOverwrite: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TAttachSecretTagsDTO = {
projectId: string;
secretName: string;
tagSlugs: string[];
environment: string;
secretPath: string;
type: SecretType;
} & Omit<TProjectPermission, "projectId">;
@@ -0,0 +1,124 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2BridgeDALFactory>;
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
// This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretVersionV2)
.where(`${TableName.SecretVersionV2}.folderId`, folderId)
.join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretVersionV2}.secretId`)
.join<TSecretVersionsV2, TSecretVersionsV2 & { secretId: string; max: number }>(
(tx || db)(TableName.SecretVersionV2)
.groupBy("folderId", "secretId")
.max("version")
.select("secretId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretVersionV2}.secretId`, "latestVersion.secretId").andOn(
`${TableName.SecretVersionV2}.version`,
"latestVersion.max"
);
}
)
.select(selectAllTableCols(TableName.SecretVersionV2));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestVersionByFolderId" });
}
};
const bulkUpdate = async (
data: Array<{ filter: Partial<TSecretVersionsV2>; data: TSecretVersionsV2Update }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.SecretVersionV2)
.where(filter)
.update(updateData)
.increment("version", 1) // TODO: Is this really needed?
.returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
try {
if (!secretIds.length) return {};
const docs: Array<TSecretVersionsV2 & { max: number }> = await (tx || db.replicaNode())(TableName.SecretVersionV2)
.where("folderId", folderId)
.whereIn(`${TableName.SecretVersionV2}.secretId`, secretIds)
.join(
(tx || db)(TableName.SecretVersionV2)
.groupBy("secretId")
.max("version")
.select("secretId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretVersionV2}.secretId`, "latestVersion.secretId").andOn(
`${TableName.SecretVersionV2}.version`,
"latestVersion.max"
);
}
);
return docs.reduce<Record<string, TSecretVersionsV2>>(
(prev, curr) => ({ ...prev, [curr.secretId || ""]: curr }),
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
}
};
const pruneExcessVersions = async () => {
try {
await db(TableName.SecretVersionV2)
.with("version_cte", (qb) => {
void qb
.from(TableName.SecretVersionV2)
.select(
"id",
"folderId",
db.raw(
`ROW_NUMBER() OVER (PARTITION BY ${TableName.SecretVersionV2}."secretId" ORDER BY ${TableName.SecretVersionV2}."createdAt" DESC) AS row_num`
)
);
})
.join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.SecretVersionV2}.folderId`)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
.join("version_cte", "version_cte.id", `${TableName.SecretVersionV2}.id`)
.whereRaw(`version_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
.delete();
} catch (error) {
throw new DatabaseError({
error,
name: "Secret Version Prune"
});
}
};
return {
...secretVersionV2Orm,
pruneExcessVersions,
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId
};
};

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