mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-28 18:55:53 +00:00
Compare commits
7 Commits
misc/add-p
...
feat/add-m
Author | SHA1 | Date | |
---|---|---|---|
|
0f710b1ccc | ||
|
71c55d5a53 | ||
|
f33a777fae | ||
|
8a870131e9 | ||
|
d97057b43b | ||
|
19b0cd9735 | ||
|
07898414a3 |
@@ -14,11 +14,3 @@ docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74
|
||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
||||
k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11
|
||||
k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52
|
||||
backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts:generic-api-key:125
|
||||
frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx:generic-api-key:67
|
||||
frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx:generic-api-key:14
|
||||
frontend/src/components/secret-rotations-v2/SecretRotationV2StatusBadge.tsx:generic-api-key:11
|
||||
frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx:generic-api-key:23
|
||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||
|
@@ -8,8 +8,7 @@ RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client \
|
||||
openssl
|
||||
openssh-client
|
||||
|
||||
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apt-get install -y \
|
||||
|
@@ -19,7 +19,6 @@ RUN apt-get update && apt-get install -y \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client \
|
||||
openssl \
|
||||
curl \
|
||||
pkg-config
|
||||
|
||||
|
@@ -9,7 +9,6 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
store[key] = value;
|
||||
return "OK";
|
||||
},
|
||||
setExpiry: async () => 0,
|
||||
setItemWithExpiry: async (key, value) => {
|
||||
store[key] = value;
|
||||
return "OK";
|
||||
|
22
backend/package-lock.json
generated
22
backend/package-lock.json
generated
@@ -132,7 +132,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
@@ -9753,12 +9753,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||
"license": "MIT",
|
||||
"version": "20.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
||||
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
@@ -20082,6 +20081,11 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/scim-patch/node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/scim2-parse-filter": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||
@@ -22438,9 +22442,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
|
@@ -89,7 +89,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -38,7 +38,6 @@ import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
@@ -207,7 +206,6 @@ declare module "fastify" {
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||
sshHost: TSshHostServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
|
28
backend/src/@types/knex.d.ts
vendored
28
backend/src/@types/knex.d.ts
vendored
@@ -232,9 +232,6 @@ import {
|
||||
TProjectSplitBackfillIds,
|
||||
TProjectSplitBackfillIdsInsert,
|
||||
TProjectSplitBackfillIdsUpdate,
|
||||
TProjectSshConfigs,
|
||||
TProjectSshConfigsInsert,
|
||||
TProjectSshConfigsUpdate,
|
||||
TProjectsUpdate,
|
||||
TProjectTemplates,
|
||||
TProjectTemplatesInsert,
|
||||
@@ -383,15 +380,6 @@ import {
|
||||
TSshCertificateTemplates,
|
||||
TSshCertificateTemplatesInsert,
|
||||
TSshCertificateTemplatesUpdate,
|
||||
TSshHostLoginUserMappings,
|
||||
TSshHostLoginUserMappingsInsert,
|
||||
TSshHostLoginUserMappingsUpdate,
|
||||
TSshHostLoginUsers,
|
||||
TSshHostLoginUsersInsert,
|
||||
TSshHostLoginUsersUpdate,
|
||||
TSshHosts,
|
||||
TSshHostsInsert,
|
||||
TSshHostsUpdate,
|
||||
TSuperAdmin,
|
||||
TSuperAdminInsert,
|
||||
TSuperAdminUpdate,
|
||||
@@ -437,7 +425,6 @@ declare module "knex/types/tables" {
|
||||
interface Tables {
|
||||
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||
[TableName.SshHost]: KnexOriginal.CompositeTableType<TSshHosts, TSshHostsInsert, TSshHostsUpdate>;
|
||||
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||
TSshCertificateAuthorities,
|
||||
TSshCertificateAuthoritiesInsert,
|
||||
@@ -463,16 +450,6 @@ declare module "knex/types/tables" {
|
||||
TSshCertificateBodiesInsert,
|
||||
TSshCertificateBodiesUpdate
|
||||
>;
|
||||
[TableName.SshHostLoginUser]: KnexOriginal.CompositeTableType<
|
||||
TSshHostLoginUsers,
|
||||
TSshHostLoginUsersInsert,
|
||||
TSshHostLoginUsersUpdate
|
||||
>;
|
||||
[TableName.SshHostLoginUserMapping]: KnexOriginal.CompositeTableType<
|
||||
TSshHostLoginUserMappings,
|
||||
TSshHostLoginUserMappingsInsert,
|
||||
TSshHostLoginUserMappingsUpdate
|
||||
>;
|
||||
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||
TCertificateAuthorities,
|
||||
TCertificateAuthoritiesInsert,
|
||||
@@ -577,11 +554,6 @@ declare module "knex/types/tables" {
|
||||
[TableName.SuperAdmin]: KnexOriginal.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
|
||||
[TableName.ApiKey]: KnexOriginal.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
|
||||
[TableName.Project]: KnexOriginal.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
|
||||
[TableName.ProjectSshConfig]: KnexOriginal.CompositeTableType<
|
||||
TProjectSshConfigs,
|
||||
TProjectSshConfigsInsert,
|
||||
TProjectSshConfigsUpdate
|
||||
>;
|
||||
[TableName.ProjectMembership]: KnexOriginal.CompositeTableType<
|
||||
TProjectMemberships,
|
||||
TProjectMembershipsInsert,
|
||||
|
@@ -1,19 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasTypeColumn = await knex.schema.hasColumn(TableName.KmsKey, "type");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||
if (!hasTypeColumn) t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||
t.dropColumn("keyUsage");
|
||||
});
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource"))) {
|
||||
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||
t.string("keySource");
|
||||
});
|
||||
|
||||
// Backfilling the keySource to internal
|
||||
await knex(TableName.SshCertificateAuthority).update({ keySource: "internal" });
|
||||
|
||||
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||
t.string("keySource").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.SshCertificate, "sshCaId")) {
|
||||
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||
t.uuid("sshCaId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource")) {
|
||||
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||
t.dropColumn("keySource");
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.SshHost))) {
|
||||
await knex.schema.createTable(TableName.SshHost, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.string("hostname").notNullable();
|
||||
t.string("userCertTtl").notNullable();
|
||||
t.string("hostCertTtl").notNullable();
|
||||
t.uuid("userSshCaId").notNullable();
|
||||
t.foreign("userSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||
t.uuid("hostSshCaId").notNullable();
|
||||
t.foreign("hostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||
t.unique(["projectId", "hostname"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SshHost);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SshHostLoginUser))) {
|
||||
await knex.schema.createTable(TableName.SshHostLoginUser, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("sshHostId").notNullable();
|
||||
t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("CASCADE");
|
||||
t.string("loginUser").notNullable(); // e.g. ubuntu, root, ec2-user, ...
|
||||
t.unique(["sshHostId", "loginUser"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SshHostLoginUser);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SshHostLoginUserMapping))) {
|
||||
await knex.schema.createTable(TableName.SshHostLoginUserMapping, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("sshHostLoginUserId").notNullable();
|
||||
t.foreign("sshHostLoginUserId").references("id").inTable(TableName.SshHostLoginUser).onDelete("CASCADE");
|
||||
t.uuid("userId").nullable();
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
t.unique(["sshHostLoginUserId", "userId"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectSshConfig))) {
|
||||
// new table to store configuration for projects of type SSH (i.e. Infisical SSH)
|
||||
await knex.schema.createTable(TableName.ProjectSshConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("defaultUserSshCaId");
|
||||
t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||
t.uuid("defaultHostSshCaId");
|
||||
t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectSshConfig);
|
||||
}
|
||||
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||
t.uuid("sshHostId").nullable();
|
||||
t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectSshConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectSshConfig);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SshHostLoginUserMapping);
|
||||
await dropOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SshHostLoginUser);
|
||||
await dropOnUpdateTrigger(knex, TableName.SshHostLoginUser);
|
||||
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId");
|
||||
if (hasColumn) {
|
||||
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||
t.dropColumn("sshHostId");
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SshHost);
|
||||
await dropOnUpdateTrigger(knex, TableName.SshHost);
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId"))) {
|
||||
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||
tb.uuid("dynamicSecretId");
|
||||
tb.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId")) {
|
||||
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||
tb.dropColumn("dynamicSecretId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.string("note").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note");
|
||||
if (hasCol) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.dropColumn("note");
|
||||
});
|
||||
}
|
||||
}
|
@@ -17,8 +17,7 @@ export const AccessApprovalRequestsSchema = z.object({
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
requestedByUserId: z.string().uuid(),
|
||||
note: z.string().nullable().optional()
|
||||
requestedByUserId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||
|
@@ -75,7 +75,6 @@ export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
export * from "./project-slack-configs";
|
||||
export * from "./project-split-backfill-ids";
|
||||
export * from "./project-ssh-configs";
|
||||
export * from "./project-templates";
|
||||
export * from "./project-user-additional-privilege";
|
||||
export * from "./project-user-membership-roles";
|
||||
@@ -126,9 +125,6 @@ export * from "./ssh-certificate-authority-secrets";
|
||||
export * from "./ssh-certificate-bodies";
|
||||
export * from "./ssh-certificate-templates";
|
||||
export * from "./ssh-certificates";
|
||||
export * from "./ssh-host-login-user-mappings";
|
||||
export * from "./ssh-host-login-users";
|
||||
export * from "./ssh-hosts";
|
||||
export * from "./super-admin";
|
||||
export * from "./totp-configs";
|
||||
export * from "./trusted-ips";
|
||||
|
@@ -16,8 +16,7 @@ export const KmsKeysSchema = z.object({
|
||||
name: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string().nullable().optional(),
|
||||
keyUsage: z.string().default("encrypt-decrypt")
|
||||
projectId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
|
@@ -2,9 +2,6 @@ import { z } from "zod";
|
||||
|
||||
export enum TableName {
|
||||
Users = "users",
|
||||
SshHost = "ssh_hosts",
|
||||
SshHostLoginUser = "ssh_host_login_users",
|
||||
SshHostLoginUserMapping = "ssh_host_login_user_mappings",
|
||||
SshCertificateAuthority = "ssh_certificate_authorities",
|
||||
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
||||
SshCertificateTemplate = "ssh_certificate_templates",
|
||||
@@ -41,7 +38,6 @@ export enum TableName {
|
||||
SuperAdmin = "super_admin",
|
||||
RateLimit = "rate_limit",
|
||||
ApiKey = "api_keys",
|
||||
ProjectSshConfig = "project_ssh_configs",
|
||||
Project = "projects",
|
||||
ProjectBot = "project_bots",
|
||||
Environment = "project_environments",
|
||||
|
@@ -1,21 +0,0 @@
|
||||
// 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 ProjectSshConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string(),
|
||||
defaultUserSshCaId: z.string().uuid().nullable().optional(),
|
||||
defaultHostSshCaId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TProjectSshConfigs = z.infer<typeof ProjectSshConfigsSchema>;
|
||||
export type TProjectSshConfigsInsert = Omit<z.input<typeof ProjectSshConfigsSchema>, TImmutableDBKeys>;
|
||||
export type TProjectSshConfigsUpdate = Partial<Omit<z.input<typeof ProjectSshConfigsSchema>, TImmutableDBKeys>>;
|
@@ -16,7 +16,8 @@ export const ResourceMetadataSchema = z.object({
|
||||
identityId: z.string().uuid().nullable().optional(),
|
||||
secretId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
dynamicSecretId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
||||
|
@@ -14,8 +14,7 @@ export const SshCertificateAuthoritiesSchema = z.object({
|
||||
projectId: z.string(),
|
||||
status: z.string(),
|
||||
friendlyName: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
keySource: z.string()
|
||||
keyAlgorithm: z.string()
|
||||
});
|
||||
|
||||
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
||||
|
@@ -11,15 +11,14 @@ export const SshCertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
sshCaId: z.string().uuid().nullable().optional(),
|
||||
sshCaId: z.string().uuid(),
|
||||
sshCertificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
serialNumber: z.string(),
|
||||
certType: z.string(),
|
||||
principals: z.string().array(),
|
||||
keyId: z.string(),
|
||||
notBefore: z.date(),
|
||||
notAfter: z.date(),
|
||||
sshHostId: z.string().uuid().nullable().optional()
|
||||
notAfter: z.date()
|
||||
});
|
||||
|
||||
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
||||
|
@@ -1,22 +0,0 @@
|
||||
// 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 SshHostLoginUserMappingsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
sshHostLoginUserId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSshHostLoginUserMappings = z.infer<typeof SshHostLoginUserMappingsSchema>;
|
||||
export type TSshHostLoginUserMappingsInsert = Omit<z.input<typeof SshHostLoginUserMappingsSchema>, TImmutableDBKeys>;
|
||||
export type TSshHostLoginUserMappingsUpdate = Partial<
|
||||
Omit<z.input<typeof SshHostLoginUserMappingsSchema>, TImmutableDBKeys>
|
||||
>;
|
@@ -1,20 +0,0 @@
|
||||
// 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 SshHostLoginUsersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
sshHostId: z.string().uuid(),
|
||||
loginUser: z.string()
|
||||
});
|
||||
|
||||
export type TSshHostLoginUsers = z.infer<typeof SshHostLoginUsersSchema>;
|
||||
export type TSshHostLoginUsersInsert = Omit<z.input<typeof SshHostLoginUsersSchema>, TImmutableDBKeys>;
|
||||
export type TSshHostLoginUsersUpdate = Partial<Omit<z.input<typeof SshHostLoginUsersSchema>, TImmutableDBKeys>>;
|
@@ -1,24 +0,0 @@
|
||||
// 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 SshHostsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string(),
|
||||
hostname: z.string(),
|
||||
userCertTtl: z.string(),
|
||||
hostCertTtl: z.string(),
|
||||
userSshCaId: z.string().uuid(),
|
||||
hostSshCaId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TSshHosts = z.infer<typeof SshHostsSchema>;
|
||||
export type TSshHostsInsert = Omit<z.input<typeof SshHostsSchema>, TImmutableDBKeys>;
|
||||
export type TSshHostsUpdate = Partial<Omit<z.input<typeof SshHostsSchema>, TImmutableDBKeys>>;
|
@@ -22,8 +22,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
body: z.object({
|
||||
permissions: z.any().array(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().optional(),
|
||||
note: z.string().max(255).optional()
|
||||
temporaryRange: z.string().optional()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
@@ -44,8 +43,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.query.projectSlug,
|
||||
temporaryRange: req.body.temporaryRange,
|
||||
isTemporary: req.body.isTemporary,
|
||||
note: req.body.note
|
||||
isTemporary: req.body.isTemporary
|
||||
});
|
||||
return { approval: request };
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
|
||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -48,7 +49,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
.nullable(),
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -143,7 +145,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
@@ -238,6 +241,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
name: req.params.name,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return { dynamicSecret: dynamicSecretCfg };
|
||||
}
|
||||
});
|
||||
|
@@ -32,7 +32,6 @@ import { registerSnapshotRouter } from "./snapshot-router";
|
||||
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
||||
import { registerSshCertRouter } from "./ssh-certificate-router";
|
||||
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||
import { registerSshHostRouter } from "./ssh-host-router";
|
||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||
|
||||
@@ -83,7 +82,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
||||
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
||||
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await sshRouter.register(registerSshHostRouter, { prefix: "/hosts" });
|
||||
},
|
||||
{ prefix: "/ssh" }
|
||||
);
|
||||
|
@@ -2,7 +2,7 @@ import z from "zod";
|
||||
|
||||
import { KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "KMIP endpoint for creating managed objects",
|
||||
body: z.object({
|
||||
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
}),
|
||||
response: {
|
||||
200: KmsKeysSchema
|
||||
@@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -136,12 +136,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/login/error",
|
||||
method: "GET",
|
||||
handler: async (req, res) => {
|
||||
const failureMessage = req.session.get<any>("messages");
|
||||
await req.session.destroy();
|
||||
|
||||
return res.status(500).send({
|
||||
error: "Authentication error",
|
||||
details: failureMessage ?? req.query
|
||||
details: req.query
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -1,15 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { normalizeSshPrivateKey } from "@app/ee/services/ssh/ssh-certificate-authority-fns";
|
||||
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||
import { SshCaKeySource, SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -21,34 +20,14 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create SSH CA",
|
||||
body: z
|
||||
.object({
|
||||
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
||||
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||
keyAlgorithm: z
|
||||
.nativeEnum(SshCertKeyAlgorithm)
|
||||
.default(SshCertKeyAlgorithm.ED25519)
|
||||
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
|
||||
publicKey: z.string().trim().optional().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.publicKey),
|
||||
privateKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((val) => (val ? normalizeSshPrivateKey(val) : undefined))
|
||||
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.privateKey),
|
||||
keySource: z
|
||||
.nativeEnum(SshCaKeySource)
|
||||
.default(SshCaKeySource.INTERNAL)
|
||||
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keySource)
|
||||
})
|
||||
.refine((data) => data.keySource === SshCaKeySource.INTERNAL || (!!data.publicKey && !!data.privateKey), {
|
||||
message: "publicKey and privateKey are required when keySource is external",
|
||||
path: ["publicKey"]
|
||||
})
|
||||
.refine((data) => data.keySource === SshCaKeySource.EXTERNAL || !!data.keyAlgorithm, {
|
||||
message: "keyAlgorithm is required when keySource is internal",
|
||||
path: ["keyAlgorithm"]
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
||||
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||
keyAlgorithm: z
|
||||
.nativeEnum(CertKeyAlgorithm)
|
||||
.default(CertKeyAlgorithm.RSA_2048)
|
||||
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
ca: sanitizedSshCa.extend({
|
||||
|
@@ -2,13 +2,13 @@ import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -108,8 +108,8 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
.min(1)
|
||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
|
||||
keyAlgorithm: z
|
||||
.nativeEnum(SshCertKeyAlgorithm)
|
||||
.default(SshCertKeyAlgorithm.ED25519)
|
||||
.nativeEnum(CertKeyAlgorithm)
|
||||
.default(CertKeyAlgorithm.RSA_2048)
|
||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
|
||||
certType: z
|
||||
.nativeEnum(SshCertType)
|
||||
@@ -133,7 +133,7 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
|
||||
publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
|
||||
keyAlgorithm: z
|
||||
.nativeEnum(SshCertKeyAlgorithm)
|
||||
.nativeEnum(CertKeyAlgorithm)
|
||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||
})
|
||||
}
|
||||
|
@@ -92,8 +92,8 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
|
||||
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
|
||||
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
||||
})
|
||||
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
|
||||
message: "Max TLL must be greater than or equal to TTL",
|
||||
.refine((data) => ms(data.maxTTL) > ms(data.ttl), {
|
||||
message: "Max TLL must be greater than TTL",
|
||||
path: ["maxTTL"]
|
||||
}),
|
||||
response: {
|
||||
|
@@ -1,444 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
|
||||
import { SSH_HOSTS } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.array(
|
||||
sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const hosts = await server.services.sshHost.listSshHosts({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return hosts;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:sshHostId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const host = await server.services.sshHost.getSshHost({
|
||||
sshHostId: req.params.sshHostId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Add an SSH Host",
|
||||
body: z.object({
|
||||
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
|
||||
hostname: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((v) => isValidHostname(v), {
|
||||
message: "Hostname must be a valid hostname"
|
||||
})
|
||||
.describe(SSH_HOSTS.CREATE.hostname),
|
||||
userCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.default("8h")
|
||||
.describe(SSH_HOSTS.CREATE.userCertTtl),
|
||||
hostCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.default("1y")
|
||||
.describe(SSH_HOSTS.CREATE.hostCertTtl),
|
||||
loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOSTS.CREATE.loginMappings),
|
||||
userSshCaId: z.string().describe(SSH_HOSTS.CREATE.userSshCaId).optional(),
|
||||
hostSshCaId: z.string().describe(SSH_HOSTS.CREATE.hostSshCaId).optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const host = await server.services.sshHost.createSshHost({
|
||||
...req.body,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname,
|
||||
userCertTtl: host.userCertTtl,
|
||||
hostCertTtl: host.hostCertTtl,
|
||||
loginMappings: host.loginMappings,
|
||||
userSshCaId: host.userSshCaId,
|
||||
hostSshCaId: host.hostSshCaId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:sshHostId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update SSH Host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
|
||||
}),
|
||||
body: z.object({
|
||||
hostname: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((v) => isValidHostname(v), {
|
||||
message: "Hostname must be a valid hostname"
|
||||
})
|
||||
.optional()
|
||||
.describe(SSH_HOSTS.UPDATE.hostname),
|
||||
userCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(SSH_HOSTS.UPDATE.userCertTtl),
|
||||
hostCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(SSH_HOSTS.UPDATE.hostCertTtl),
|
||||
loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOSTS.UPDATE.loginMappings)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const host = await server.services.sshHost.updateSshHost({
|
||||
sshHostId: req.params.sshHostId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname,
|
||||
userCertTtl: host.userCertTtl,
|
||||
hostCertTtl: host.hostCertTtl,
|
||||
loginMappings: host.loginMappings,
|
||||
userSshCaId: host.userSshCaId,
|
||||
hostSshCaId: host.hostSshCaId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:sshHostId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const host = await server.services.sshHost.deleteSshHost({
|
||||
sshHostId: req.params.sshHostId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:sshHostId/issue-user-cert",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
description: "Issue SSH certificate for user",
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
|
||||
}),
|
||||
body: z.object({
|
||||
loginUser: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.loginUser)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
serialNumber: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.serialNumber),
|
||||
signedKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.signedKey),
|
||||
privateKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.privateKey),
|
||||
publicKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.publicKey),
|
||||
keyAlgorithm: z.nativeEnum(SshCertKeyAlgorithm).describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } =
|
||||
await server.services.sshHost.issueSshHostUserCert({
|
||||
sshHostId: req.params.sshHostId,
|
||||
loginUser: req.body.loginUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ISSUE_SSH_HOST_USER_CERT,
|
||||
metadata: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
hostname: host.hostname,
|
||||
loginUser: req.body.loginUser,
|
||||
principals,
|
||||
ttl: host.userCertTtl
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueSshHostUserCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
hostname: host.hostname,
|
||||
principals,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
signedKey: signedPublicKey,
|
||||
privateKey,
|
||||
publicKey,
|
||||
keyAlgorithm
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:sshHostId/issue-host-cert",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Issue SSH certificate for host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.sshHostId)
|
||||
}),
|
||||
body: z.object({
|
||||
publicKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.publicKey)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
serialNumber: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.serialNumber),
|
||||
signedKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.signedKey)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { host, principals, serialNumber, signedPublicKey } = await server.services.sshHost.issueSshHostHostCert({
|
||||
sshHostId: req.params.sshHostId,
|
||||
publicKey: req.body.publicKey,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ISSUE_SSH_HOST_HOST_CERT,
|
||||
metadata: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
hostname: host.hostname,
|
||||
principals,
|
||||
serialNumber,
|
||||
ttl: host.hostCertTtl
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueSshHostHostCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
hostname: host.hostname,
|
||||
principals,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
signedKey: signedPublicKey
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:sshHostId/user-ca-public-key",
|
||||
config: {
|
||||
rateLimit: publicSshCaLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get public key of the user SSH CA linked to the host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
|
||||
}),
|
||||
response: {
|
||||
200: z.string().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.publicKey)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const publicKey = await server.services.sshHost.getSshHostUserCaPk(req.params.sshHostId);
|
||||
return publicKey;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:sshHostId/host-ca-public-key",
|
||||
config: {
|
||||
rateLimit: publicSshCaLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get public key of the host SSH CA linked to the host",
|
||||
params: z.object({
|
||||
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
|
||||
}),
|
||||
response: {
|
||||
200: z.string().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.publicKey)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const publicKey = await server.services.sshHost.getSshHostHostCaPk(req.params.sshHostId);
|
||||
return publicKey;
|
||||
}
|
||||
});
|
||||
};
|
@@ -94,8 +94,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug,
|
||||
note
|
||||
projectSlug
|
||||
}: TCreateAccessApprovalRequestDTO) => {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
@@ -210,8 +209,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
requestedByUserId: actorId,
|
||||
temporaryRange: temporaryRange || null,
|
||||
permissions: JSON.stringify(requestedPermissions),
|
||||
isTemporary,
|
||||
note: note || null
|
||||
isTemporary
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -234,8 +232,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
note
|
||||
approvalUrl
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -255,8 +252,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl,
|
||||
note
|
||||
approvalUrl
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequest
|
||||
});
|
||||
|
@@ -24,7 +24,6 @@ export type TCreateAccessApprovalRequestDTO = {
|
||||
permissions: unknown;
|
||||
isTemporary: boolean;
|
||||
temporaryRange?: string;
|
||||
note?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
|
@@ -10,10 +10,8 @@ import {
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
@@ -191,12 +189,6 @@ export enum EventType {
|
||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||
CREATE_SSH_HOST = "create-ssh-host",
|
||||
UPDATE_SSH_HOST = "update-ssh-host",
|
||||
DELETE_SSH_HOST = "delete-ssh-host",
|
||||
GET_SSH_HOST = "get-ssh-host",
|
||||
ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert",
|
||||
ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert",
|
||||
CREATE_CA = "create-certificate-authority",
|
||||
GET_CA = "get-certificate-authority",
|
||||
UPDATE_CA = "update-certificate-authority",
|
||||
@@ -256,11 +248,6 @@ export enum EventType {
|
||||
GET_CMEK = "get-cmek",
|
||||
CMEK_ENCRYPT = "cmek-encrypt",
|
||||
CMEK_DECRYPT = "cmek-decrypt",
|
||||
CMEK_SIGN = "cmek-sign",
|
||||
CMEK_VERIFY = "cmek-verify",
|
||||
CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms",
|
||||
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
|
||||
|
||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||
@@ -1390,7 +1377,7 @@ interface IssueSshCreds {
|
||||
type: EventType.ISSUE_SSH_CREDS;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
keyAlgorithm: SshCertKeyAlgorithm;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
certType: SshCertType;
|
||||
principals: string[];
|
||||
ttl: string;
|
||||
@@ -1486,80 +1473,6 @@ interface DeleteSshCertificateTemplate {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSshHost {
|
||||
type: EventType.CREATE_SSH_HOST;
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
userSshCaId: string;
|
||||
hostSshCaId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSshHost {
|
||||
type: EventType.UPDATE_SSH_HOST;
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
userSshCaId?: string;
|
||||
hostSshCaId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSshHost {
|
||||
type: EventType.DELETE_SSH_HOST;
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSshHost {
|
||||
type: EventType.GET_SSH_HOST;
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IssueSshHostUserCert {
|
||||
type: EventType.ISSUE_SSH_HOST_USER_CERT;
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
loginUser: string;
|
||||
principals: string[];
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IssueSshHostHostCert {
|
||||
type: EventType.ISSUE_SSH_HOST_HOST_CERT;
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
serialNumber: string;
|
||||
principals: string[];
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateCa {
|
||||
type: EventType.CREATE_CA;
|
||||
metadata: {
|
||||
@@ -2003,7 +1916,7 @@ interface CreateCmekEvent {
|
||||
keyId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||
encryptionAlgorithm: SymmetricEncryption;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2051,39 +1964,6 @@ interface CmekDecryptEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekSignEvent {
|
||||
type: EventType.CMEK_SIGN;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
signature: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekVerifyEvent {
|
||||
type: EventType.CMEK_VERIFY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
signature: string;
|
||||
signatureValid: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekListSigningAlgorithmsEvent {
|
||||
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekGetPublicKeyEvent {
|
||||
type: EventType.CMEK_GET_PUBLIC_KEY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetExternalGroupOrgRoleMappingsEvent {
|
||||
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||
metadata?: Record<string, never>; // not needed, based off orgId
|
||||
@@ -2613,12 +2493,6 @@ export type Event =
|
||||
| UpdateSshCertificateTemplate
|
||||
| GetSshCertificateTemplate
|
||||
| DeleteSshCertificateTemplate
|
||||
| CreateSshHost
|
||||
| UpdateSshHost
|
||||
| DeleteSshHost
|
||||
| GetSshHost
|
||||
| IssueSshHostUserCert
|
||||
| IssueSshHostHostCert
|
||||
| CreateCa
|
||||
| GetCa
|
||||
| UpdateCa
|
||||
@@ -2678,10 +2552,6 @@ export type Event =
|
||||
| GetCmeksEvent
|
||||
| CmekEncryptEvent
|
||||
| CmekDecryptEvent
|
||||
| CmekSignEvent
|
||||
| CmekVerifyEvent
|
||||
| CmekListSigningAlgorithmsEvent
|
||||
| CmekGetPublicKeyEvent
|
||||
| GetExternalGroupOrgRoleMappingsEvent
|
||||
| UpdateExternalGroupOrgRoleMappingsEvent
|
||||
| GetProjectTemplatesEvent
|
||||
|
@@ -78,10 +78,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
@@ -102,6 +98,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
||||
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
||||
@@ -159,10 +164,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@@ -187,7 +188,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||
}
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||
id: dynamicSecretLease.dynamicSecretId,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (!dynamicSecretCfg)
|
||||
throw new NotFoundError({
|
||||
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||
@@ -239,10 +258,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@@ -259,7 +274,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||
id: dynamicSecretLease.dynamicSecretId,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (!dynamicSecretCfg)
|
||||
throw new NotFoundError({
|
||||
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||
@@ -309,10 +342,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder)
|
||||
@@ -326,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||
return dynamicSecretLeases;
|
||||
};
|
||||
@@ -352,10 +390,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
||||
@@ -364,6 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!dynamicSecretLease)
|
||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||
id: dynamicSecretLease.dynamicSecretId,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (!dynamicSecretCfg)
|
||||
throw new NotFoundError({
|
||||
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
return dynamicSecretLease;
|
||||
};
|
||||
|
||||
|
@@ -1,9 +1,17 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TDynamicSecrets } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import {
|
||||
buildFindFilter,
|
||||
ormify,
|
||||
prependTableNameToFindFilter,
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
TFindFilter,
|
||||
TFindOpt
|
||||
} from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
@@ -12,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory
|
||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.DynamicSecret);
|
||||
|
||||
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => {
|
||||
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||
.leftJoin(
|
||||
TableName.ResourceMetadata,
|
||||
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||
`${TableName.DynamicSecret}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.DynamicSecret))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
)
|
||||
.where(prependTableNameToFindFilter(TableName.DynamicSecret, filter));
|
||||
|
||||
const docs = sqlNestRelationships({
|
||||
data: await query,
|
||||
key: "id",
|
||||
parentMapper: (el) => el,
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "metadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return docs[0];
|
||||
};
|
||||
|
||||
const findWithMetadata = async (
|
||||
filter: TFindFilter<TDynamicSecrets>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {}
|
||||
) => {
|
||||
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||
.leftJoin(
|
||||
TableName.ResourceMetadata,
|
||||
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||
`${TableName.DynamicSecret}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.DynamicSecret))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter));
|
||||
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
|
||||
const docs = sqlNestRelationships({
|
||||
data: await query,
|
||||
key: "id",
|
||||
parentMapper: (el) => el,
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "metadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return docs;
|
||||
};
|
||||
|
||||
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||
const listDynamicSecretsByFolderIds = async (
|
||||
{
|
||||
@@ -39,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
||||
}
|
||||
})
|
||||
.leftJoin(
|
||||
TableName.ResourceMetadata,
|
||||
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||
`${TableName.DynamicSecret}.id`
|
||||
)
|
||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.DynamicSecret),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
|
||||
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`),
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
)
|
||||
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||
|
||||
let queryWithLimit;
|
||||
if (limit) {
|
||||
const rankOffset = offset + 1;
|
||||
return await (tx || db)
|
||||
queryWithLimit = (tx || db.replicaNode())
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
@@ -58,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
.andWhere("w.rank", "<", rankOffset + limit);
|
||||
}
|
||||
|
||||
const dynamicSecrets = await query;
|
||||
const dynamicSecrets = sqlNestRelationships({
|
||||
data: await (queryWithLimit || query),
|
||||
key: "id",
|
||||
parentMapper: (el) => el,
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "metadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return dynamicSecrets;
|
||||
} catch (error) {
|
||||
@@ -66,5 +178,5 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, listDynamicSecretsByFolderIds };
|
||||
return { ...orm, listDynamicSecretsByFolderIds, findOne, findWithMetadata };
|
||||
};
|
||||
|
@@ -12,6 +12,7 @@ import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||
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 { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
@@ -46,6 +47,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@@ -60,7 +62,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
projectGatewayDAL,
|
||||
resourceMetadataDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@@ -73,7 +76,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectSlug,
|
||||
actorOrgId,
|
||||
defaultTTL,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
}: TCreateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@@ -87,9 +91,10 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path, metadata })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@@ -131,16 +136,36 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||
const cfg = await dynamicSecretDAL.create(
|
||||
{
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (metadata) {
|
||||
await resourceMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
dynamicSecretId: cfg.id,
|
||||
orgId: actorOrgId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return cfg;
|
||||
});
|
||||
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
@@ -156,7 +181,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorId,
|
||||
newName,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
}: TUpdateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@@ -171,10 +197,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan?.dynamicSecret) {
|
||||
@@ -193,6 +215,27 @@ export const dynamicSecretServiceFactory = ({
|
||||
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
if (metadata) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (newName) {
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
@@ -231,14 +274,41 @@ export const dynamicSecretServiceFactory = ({
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||
const cfg = await dynamicSecretDAL.updateById(
|
||||
dynamicSecretCfg.id,
|
||||
{
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
|
||||
.cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (metadata) {
|
||||
await resourceMetadataDAL.delete(
|
||||
{
|
||||
dynamicSecretId: cfg.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await resourceMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
dynamicSecretId: cfg.id,
|
||||
orgId: actorOrgId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return cfg;
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
@@ -268,10 +338,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder)
|
||||
@@ -282,6 +348,15 @@ export const dynamicSecretServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||
// when not forced we check with the external system to first remove the things
|
||||
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
|
||||
@@ -329,14 +404,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder)
|
||||
@@ -346,6 +413,25 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (!dynamicSecretCfg) {
|
||||
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
@@ -356,6 +442,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
) as object;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
};
|
||||
|
||||
@@ -426,7 +513,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
ProjectPermissionSub.DynamicSecrets
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
@@ -473,16 +560,12 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder)
|
||||
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findWithMetadata(
|
||||
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||
{
|
||||
limit,
|
||||
@@ -490,7 +573,17 @@ export const dynamicSecretServiceFactory = ({
|
||||
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
||||
}
|
||||
);
|
||||
return dynamicSecretCfg;
|
||||
|
||||
return dynamicSecretCfg.filter((dynamicSecret) => {
|
||||
return permission.can(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecret.metadata
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const listDynamicSecretsByFolderIds = async (
|
||||
@@ -542,24 +635,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
isInternal,
|
||||
...params
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
// verify user has access to each env in request
|
||||
environmentSlugs.forEach((environmentSlug) =>
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
)
|
||||
);
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||
if (!folders.length)
|
||||
@@ -572,7 +655,16 @@ export const dynamicSecretServiceFactory = ({
|
||||
...params
|
||||
});
|
||||
|
||||
return dynamicSecretCfg;
|
||||
return dynamicSecretCfg.filter((dynamicSecret) => {
|
||||
return permission.can(
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: dynamicSecret.environment,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecret.metadata
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async ({
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||
@@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = {
|
||||
environmentSlug: string;
|
||||
name: string;
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateDynamicSecretDTO = {
|
||||
@@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = {
|
||||
environmentSlug: string;
|
||||
inputs?: TProvider["inputs"];
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteDynamicSecretDTO = {
|
||||
|
@@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -115,7 +115,6 @@ export const externalKmsServiceFactory = ({
|
||||
{
|
||||
isReserved: false,
|
||||
description,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
name: kmsName,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
|
@@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
plaintext: data
|
||||
});
|
||||
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
|
||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) };
|
||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) };
|
||||
};
|
||||
|
||||
const decrypt = async (encryptedBlob: Buffer) => {
|
||||
@@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
ciphertext: encryptedBlob
|
||||
});
|
||||
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
|
||||
return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) };
|
||||
return { data: Buffer.from(decryptedText[0].plaintext) };
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
||||
const decrypt: {
|
||||
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||
if (!pkcs11 || !isInitialized) {
|
||||
throw new Error("PKCS#11 module is not initialized");
|
||||
}
|
||||
@@ -309,10 +309,10 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
||||
|
||||
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
||||
|
||||
const tempBuffer: Buffer = Buffer.alloc(encryptedData.length);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const tempBuffer = Buffer.alloc(encryptedData.length);
|
||||
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
||||
|
||||
// Create a new buffer from the decrypted data
|
||||
return Buffer.from(decryptedData);
|
||||
} catch (error) {
|
||||
logger.error(error, "HSM: Failed to perform decryption");
|
||||
|
@@ -3,7 +3,6 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -404,7 +403,6 @@ export const kmipOperationServiceFactory = ({
|
||||
algorithm,
|
||||
isReserved: false,
|
||||
projectId,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
orgId: project.orgId
|
||||
});
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
@@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TKmipCreateDTO = {
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
algorithm: SymmetricEncryption;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipGetDTO = {
|
||||
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
|
||||
export type TKmipRegisterDTO = {
|
||||
name: string;
|
||||
key: string;
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
algorithm: SymmetricEncryption;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TSetupOrgKmipDTO = {
|
||||
|
@@ -32,9 +32,7 @@ export enum ProjectPermissionCmekActions {
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt",
|
||||
Sign = "sign",
|
||||
Verify = "verify"
|
||||
Decrypt = "decrypt"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionDynamicSecretActions {
|
||||
@@ -69,14 +67,6 @@ export enum ProjectPermissionGroupActions {
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSshHostActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
IssueHostCert = "issue-host-cert"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretSyncActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -131,7 +121,6 @@ export enum ProjectPermissionSub {
|
||||
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||
SshCertificates = "ssh-certificates",
|
||||
SshCertificateTemplates = "ssh-certificate-templates",
|
||||
SshHosts = "ssh-hosts",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms",
|
||||
@@ -155,6 +144,10 @@ export type SecretFolderSubjectFields = {
|
||||
export type DynamicSecretSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
metadata?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type SecretImportSubjectFields = {
|
||||
@@ -171,10 +164,6 @@ export type IdentityManagementSubjectFields = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type SshHostSubjectFields = {
|
||||
hostname: string;
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionSecretActions,
|
||||
@@ -230,10 +219,6 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||
| [
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||
@@ -284,6 +269,42 @@ const SecretConditionV1Schema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const DynamicSecretConditionV2Schema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
|
||||
metadata: z.object({
|
||||
[PermissionConditionOperators.$ELEMENTMATCH]: z
|
||||
.object({
|
||||
key: z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial(),
|
||||
value: z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
})
|
||||
.partial()
|
||||
})
|
||||
})
|
||||
.partial();
|
||||
|
||||
const SecretConditionV2Schema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
@@ -332,21 +353,6 @@ const IdentityManagementConditionSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const SshHostConditionSchema = z
|
||||
.object({
|
||||
hostname: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
const GeneralPermissionSchema = [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||
@@ -581,7 +587,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: SecretConditionV1Schema.describe(
|
||||
conditions: DynamicSecretConditionV2Schema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
@@ -595,16 +601,6 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSshHostActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
conditions: SshHostConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
@@ -657,17 +653,6 @@ const buildAdminPermissionRules = () => {
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSshHostActions.Edit,
|
||||
ProjectPermissionSshHostActions.Read,
|
||||
ProjectPermissionSshHostActions.Create,
|
||||
ProjectPermissionSshHostActions.Delete,
|
||||
ProjectPermissionSshHostActions.IssueHostCert
|
||||
],
|
||||
ProjectPermissionSub.SshHosts
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
@@ -734,9 +719,7 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
@@ -930,8 +913,6 @@ const buildMemberPermissionRules = () => {
|
||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
|
||||
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
@@ -939,9 +920,7 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
@@ -113,13 +113,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
TSecretV2BridgeDALFactory,
|
||||
| "insertMany"
|
||||
| "upsertSecretReferences"
|
||||
| "findBySecretKeys"
|
||||
| "bulkUpdate"
|
||||
| "deleteMany"
|
||||
| "find"
|
||||
| "invalidateSecretCacheByProjectId"
|
||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
@@ -268,14 +262,13 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
id: el.id,
|
||||
version: el.version,
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||
secretValue:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
el.secret && el.secret.isRotatedSecret
|
||||
? undefined
|
||||
: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "",
|
||||
isRotatedSecret: el.secret.isRotatedSecret,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
secretValue: el.secret.isRotatedSecret
|
||||
? undefined
|
||||
: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: "",
|
||||
@@ -622,7 +615,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => {
|
||||
const encryptedValue =
|
||||
!el.secret?.isRotatedSecret && typeof el.encryptedValue !== "undefined"
|
||||
!el.secret.isRotatedSecret && typeof el.encryptedValue !== "undefined"
|
||||
? {
|
||||
encryptedValue: el.encryptedValue as Buffer,
|
||||
references: el.encryptedValue
|
||||
@@ -870,7 +863,6 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
|
||||
if (!folder) {
|
||||
|
@@ -45,14 +45,7 @@ type TSecretReplicationServiceFactoryDep = {
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
TSecretV2BridgeDALFactory,
|
||||
| "find"
|
||||
| "findBySecretKeys"
|
||||
| "insertMany"
|
||||
| "bulkUpdate"
|
||||
| "delete"
|
||||
| "upsertSecretReferences"
|
||||
| "transaction"
|
||||
| "invalidateSecretCacheByProjectId"
|
||||
"find" | "findBySecretKeys" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<
|
||||
TSecretVersionV2DALFactory,
|
||||
@@ -267,7 +260,6 @@ export const secretReplicationServiceFactory = ({
|
||||
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
||||
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
||||
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
||||
projectId,
|
||||
secretImports: sourceSecretImports,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
@@ -505,7 +497,6 @@ export const secretReplicationServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
await secretQueueService.syncSecrets({
|
||||
projectId,
|
||||
orgId,
|
||||
|
@@ -88,7 +88,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
TSecretV2BridgeDALFactory,
|
||||
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "invalidateSecretCacheByProjectId"
|
||||
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
@@ -515,7 +515,6 @@ export const secretRotationV2ServiceFactory = ({
|
||||
});
|
||||
});
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
await snapshotService.performSnapshot(folder.id);
|
||||
await secretQueueService.syncSecrets({
|
||||
orgId: connection.orgId,
|
||||
@@ -652,7 +651,6 @@ export const secretRotationV2ServiceFactory = ({
|
||||
});
|
||||
|
||||
if (secretsMappingUpdated) {
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
await snapshotService.performSnapshot(folder.id);
|
||||
await secretQueueService.syncSecrets({
|
||||
orgId: connection.orgId,
|
||||
@@ -779,7 +777,6 @@ export const secretRotationV2ServiceFactory = ({
|
||||
}
|
||||
|
||||
if (deleteSecrets) {
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
await snapshotService.performSnapshot(folder.id);
|
||||
await secretQueueService.syncSecrets({
|
||||
orgId: connection.orgId,
|
||||
@@ -938,7 +935,6 @@ export const secretRotationV2ServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
await snapshotService.performSnapshot(folder.id);
|
||||
await secretQueueService.syncSecrets({
|
||||
orgId: connection.orgId,
|
||||
|
@@ -48,7 +48,7 @@ type TSecretRotationQueueFactoryDep = {
|
||||
secretRotationDAL: TSecretRotationDALFactory;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "find" | "invalidateSecretCacheByProjectId">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "find">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
||||
@@ -339,8 +339,6 @@ export const secretRotationQueueFactory = ({
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId);
|
||||
} else {
|
||||
if (!botKey)
|
||||
throw new NotFoundError({
|
||||
|
@@ -1,7 +0,0 @@
|
||||
export enum SshCertKeyAlgorithm {
|
||||
RSA_2048 = "RSA_2048",
|
||||
RSA_4096 = "RSA_4096",
|
||||
ECDSA_P256 = "EC_prime256v1",
|
||||
ECDSA_P384 = "EC_secp384r1",
|
||||
ED25519 = "ED25519"
|
||||
}
|
@@ -1,193 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { groupBy, unique } from "@app/lib/fn";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
|
||||
|
||||
export const sshHostDALFactory = (db: TDbClient) => {
|
||||
const sshHostOrm = ormify(db, TableName.SshHost);
|
||||
|
||||
const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => {
|
||||
try {
|
||||
const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first();
|
||||
|
||||
if (!user) {
|
||||
throw new DatabaseError({ name: `${TableName.Users}: UserNotFound`, error: new Error("User not found") });
|
||||
}
|
||||
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
|
||||
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||
)
|
||||
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||
|
||||
const grouped = groupBy(rows, (r) => r.sshHostId);
|
||||
return Object.values(grouped).map((hostRows) => {
|
||||
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: [user.username]
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
id: sshHostId,
|
||||
hostname,
|
||||
projectId,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
userSshCaId,
|
||||
hostSshCaId
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithPrincipalsAcrossProjects` });
|
||||
}
|
||||
};
|
||||
|
||||
const findSshHostsWithLoginMappings = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.SshHost}.projectId`, projectId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||
)
|
||||
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||
|
||||
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
||||
return Object.values(hostsGrouped).map((hostRows) => {
|
||||
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(
|
||||
hostRows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
id: sshHostId,
|
||||
hostname,
|
||||
projectId,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
userSshCaId,
|
||||
hostSshCaId
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithLoginMappings` });
|
||||
}
|
||||
};
|
||||
|
||||
const findSshHostByIdWithLoginMappings = async (sshHostId: string, tx?: Knex) => {
|
||||
try {
|
||||
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||
.leftJoin(
|
||||
TableName.SshHostLoginUserMapping,
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.SshHost}.id`, sshHostId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||
);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(
|
||||
rows.filter((r) => r.loginUser),
|
||||
(r) => r.loginUser
|
||||
);
|
||||
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
id,
|
||||
projectId,
|
||||
hostname,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
userSshCaId,
|
||||
hostSshCaId
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...sshHostOrm,
|
||||
findSshHostsWithLoginMappings,
|
||||
findUserAccessibleSshHosts,
|
||||
findSshHostByIdWithLoginMappings
|
||||
};
|
||||
};
|
@@ -1,10 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSshHostLoginUserMappingDALFactory = ReturnType<typeof sshHostLoginUserMappingDALFactory>;
|
||||
|
||||
export const sshHostLoginUserMappingDALFactory = (db: TDbClient) => {
|
||||
const sshHostLoginUserMappingOrm = ormify(db, TableName.SshHostLoginUserMapping);
|
||||
return sshHostLoginUserMappingOrm;
|
||||
};
|
@@ -1,20 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SshHostsSchema } from "@app/db/schemas";
|
||||
|
||||
export const sanitizedSshHost = SshHostsSchema.pick({
|
||||
id: true,
|
||||
projectId: true,
|
||||
hostname: true,
|
||||
userCertTtl: true,
|
||||
hostCertTtl: true,
|
||||
userSshCaId: true,
|
||||
hostSshCaId: true
|
||||
});
|
||||
|
||||
export const loginMappingSchema = z.object({
|
||||
loginUser: z.string().trim(),
|
||||
allowedPrincipals: z.object({
|
||||
usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames)))
|
||||
})
|
||||
});
|
@@ -1,694 +0,0 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
|
||||
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||
import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
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 { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import {
|
||||
convertActorToPrincipals,
|
||||
createSshCert,
|
||||
createSshKeyPair,
|
||||
getSshPublicKey
|
||||
} from "../ssh/ssh-certificate-authority-fns";
|
||||
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||
import {
|
||||
TCreateSshHostDTO,
|
||||
TDeleteSshHostDTO,
|
||||
TGetSshHostDTO,
|
||||
TIssueSshHostHostCertDTO,
|
||||
TIssueSshHostUserCertDTO,
|
||||
TListSshHostsDTO,
|
||||
TUpdateSshHostDTO
|
||||
} from "./ssh-host-types";
|
||||
|
||||
type TSshHostServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "find">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
|
||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||
sshHostDAL: Pick<
|
||||
TSshHostDALFactory,
|
||||
| "transaction"
|
||||
| "create"
|
||||
| "findById"
|
||||
| "updateById"
|
||||
| "deleteById"
|
||||
| "findOne"
|
||||
| "findSshHostByIdWithLoginMappings"
|
||||
| "findUserAccessibleSshHosts"
|
||||
>;
|
||||
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
||||
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
||||
|
||||
export const sshHostServiceFactory = ({
|
||||
userDAL,
|
||||
projectDAL,
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
sshCertificateDAL,
|
||||
sshCertificateBodyDAL,
|
||||
sshHostDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
sshHostLoginUserDAL,
|
||||
permissionService,
|
||||
kmsService
|
||||
}: TSshHostServiceFactoryDep) => {
|
||||
/**
|
||||
* Return list of all SSH hosts that a user can issue user SSH certificates for
|
||||
* (i.e. is able to access / connect to) across all SSH projects in the organization
|
||||
*/
|
||||
const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => {
|
||||
if (actor !== ActorType.USER) {
|
||||
// (dangtony98): only support user for now
|
||||
throw new BadRequestError({ message: `Actor type ${actor} not supported` });
|
||||
}
|
||||
|
||||
const sshProjects = await projectDAL.find({
|
||||
orgId: actorOrgId,
|
||||
type: ProjectType.SSH
|
||||
});
|
||||
|
||||
const allowedHosts = [];
|
||||
|
||||
for await (const project of sshProjects) {
|
||||
try {
|
||||
await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
const projectHosts = await sshHostDAL.findUserAccessibleSshHosts([project.id], actorId);
|
||||
|
||||
allowedHosts.push(...projectHosts);
|
||||
} catch {
|
||||
// intentionally ignore projects where user lacks access
|
||||
}
|
||||
}
|
||||
|
||||
return allowedHosts;
|
||||
};
|
||||
|
||||
const createSshHost = async ({
|
||||
projectId,
|
||||
hostname,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
userSshCaId: requestedUserSshCaId,
|
||||
hostSshCaId: requestedHostSshCaId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateSshHostDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.Create,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname
|
||||
})
|
||||
);
|
||||
|
||||
const resolveSshCaId = async ({
|
||||
requestedId,
|
||||
fallbackId,
|
||||
label
|
||||
}: {
|
||||
requestedId?: string;
|
||||
fallbackId?: string | null;
|
||||
label: "User" | "Host";
|
||||
}) => {
|
||||
const finalId = requestedId ?? fallbackId;
|
||||
if (!finalId) {
|
||||
throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` });
|
||||
}
|
||||
|
||||
const ca = await sshCertificateAuthorityDAL.findOne({
|
||||
id: finalId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!ca) {
|
||||
throw new BadRequestError({
|
||||
message: `${label} SSH CA with ID '${finalId}' not found in project '${projectId}'`
|
||||
});
|
||||
}
|
||||
|
||||
return ca.id;
|
||||
};
|
||||
|
||||
const projectSshConfig = await projectSshConfigDAL.findOne({ projectId });
|
||||
|
||||
const userSshCaId = await resolveSshCaId({
|
||||
requestedId: requestedUserSshCaId,
|
||||
fallbackId: projectSshConfig?.defaultUserSshCaId,
|
||||
label: "User"
|
||||
});
|
||||
|
||||
const hostSshCaId = await resolveSshCaId({
|
||||
requestedId: requestedHostSshCaId,
|
||||
fallbackId: projectSshConfig?.defaultHostSshCaId,
|
||||
label: "Host"
|
||||
});
|
||||
|
||||
const newSshHost = await sshHostDAL.transaction(async (tx) => {
|
||||
const host = await sshHostDAL.create(
|
||||
{
|
||||
projectId,
|
||||
hostname,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
userSshCaId,
|
||||
hostSshCaId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// (dangtony98): room to optimize
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
{
|
||||
sshHostId: host.id,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
// check that each user has access to the SSH project
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
|
||||
if (!newSshHostWithLoginMappings) {
|
||||
throw new NotFoundError({ message: `SSH host with ID '${host.id}' not found` });
|
||||
}
|
||||
|
||||
return newSshHostWithLoginMappings;
|
||||
});
|
||||
|
||||
return newSshHost;
|
||||
};
|
||||
|
||||
const updateSshHost = async ({
|
||||
sshHostId,
|
||||
hostname,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateSshHostDTO) => {
|
||||
const host = await sshHostDAL.findById(sshHostId);
|
||||
if (!host) throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: host.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.Edit,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
|
||||
const updatedHost = await sshHostDAL.transaction(async (tx) => {
|
||||
await sshHostDAL.updateById(
|
||||
sshHostId,
|
||||
{
|
||||
hostname,
|
||||
userCertTtl,
|
||||
hostCertTtl
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (loginMappings) {
|
||||
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
|
||||
if (loginMappings.length) {
|
||||
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||
{
|
||||
sshHostId: host.id,
|
||||
loginUser
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: allowedPrincipals.usernames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const foundUsernames = new Set(users.map((u) => u.username));
|
||||
|
||||
for (const uname of allowedPrincipals.usernames) {
|
||||
if (!foundUsernames.has(uname)) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid username: ${uname}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const user of users) {
|
||||
await permissionService.getUserProjectPermission({
|
||||
userId: user.id,
|
||||
projectId: host.projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
users.map((user) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId, tx);
|
||||
if (!updatedHostWithLoginMappings) {
|
||||
throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` });
|
||||
}
|
||||
|
||||
return updatedHostWithLoginMappings;
|
||||
});
|
||||
|
||||
return updatedHost;
|
||||
};
|
||||
|
||||
const deleteSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteSshHostDTO) => {
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||
if (!host) throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: host.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.Delete,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
|
||||
await sshHostDAL.deleteById(sshHostId);
|
||||
|
||||
return host;
|
||||
};
|
||||
|
||||
const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${sshHostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: host.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.Read,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
|
||||
return host;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return SSH certificate and corresponding new SSH public-private key pair where
|
||||
* SSH public key is signed using CA behind SSH certificate with name [templateName].
|
||||
*
|
||||
* Note: Used for issuing SSH credentials as part of request against a specific SSH Host.
|
||||
*/
|
||||
const issueSshHostUserCert = async ({
|
||||
sshHostId,
|
||||
loginUser,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TIssueSshHostUserCertDTO) => {
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${sshHostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: host.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
const internalPrincipals = await convertActorToPrincipals({
|
||||
actor,
|
||||
actorId,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const mapping = host.loginMappings.find(
|
||||
(m) =>
|
||||
m.loginUser === loginUser &&
|
||||
m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed))
|
||||
);
|
||||
|
||||
if (!mapping) {
|
||||
throw new UnauthorizedError({
|
||||
message: `You are not allowed to login as ${loginUser} on this host`
|
||||
});
|
||||
}
|
||||
|
||||
const keyId = `${actor}-${actorId}`;
|
||||
|
||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: host.projectId
|
||||
});
|
||||
|
||||
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
// (dangtony98): will support more algorithms in the future
|
||||
const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
|
||||
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||
|
||||
// (dangtony98): include the loginUser as a principal on the issued certificate
|
||||
const principals = [...internalPrincipals, loginUser];
|
||||
|
||||
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||
clientPublicKey: publicKey,
|
||||
keyId,
|
||||
principals,
|
||||
requestedTtl: host.userCertTtl,
|
||||
certType: SshCertType.USER
|
||||
});
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: host.projectId
|
||||
});
|
||||
|
||||
const encryptedCertificate = secretManagerEncryptor({
|
||||
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||
}).cipherTextBlob;
|
||||
|
||||
await sshCertificateDAL.transaction(async (tx) => {
|
||||
const cert = await sshCertificateDAL.create(
|
||||
{
|
||||
sshCaId: host.userSshCaId,
|
||||
sshHostId: host.id,
|
||||
serialNumber,
|
||||
certType: SshCertType.USER,
|
||||
principals,
|
||||
keyId,
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(Date.now() + ttl * 1000)
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await sshCertificateBodyDAL.create(
|
||||
{
|
||||
sshCertId: cert.id,
|
||||
encryptedCertificate
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
host,
|
||||
principals,
|
||||
serialNumber,
|
||||
signedPublicKey,
|
||||
privateKey,
|
||||
publicKey,
|
||||
ttl,
|
||||
keyAlgorithm
|
||||
};
|
||||
};
|
||||
|
||||
const issueSshHostHostCert = async ({
|
||||
sshHostId,
|
||||
publicKey,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TIssueSshHostHostCertDTO) => {
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${sshHostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: host.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.IssueHostCert,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
|
||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: host.projectId
|
||||
});
|
||||
|
||||
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const principals = [host.hostname];
|
||||
const keyId = `host-${host.id}`;
|
||||
|
||||
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||
clientPublicKey: publicKey,
|
||||
keyId,
|
||||
principals,
|
||||
requestedTtl: host.hostCertTtl,
|
||||
certType: SshCertType.HOST
|
||||
});
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: host.projectId
|
||||
});
|
||||
|
||||
const encryptedCertificate = secretManagerEncryptor({
|
||||
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||
}).cipherTextBlob;
|
||||
|
||||
await sshCertificateDAL.transaction(async (tx) => {
|
||||
const cert = await sshCertificateDAL.create(
|
||||
{
|
||||
sshCaId: host.hostSshCaId,
|
||||
sshHostId: host.id,
|
||||
serialNumber,
|
||||
certType: SshCertType.HOST,
|
||||
principals,
|
||||
keyId,
|
||||
notBefore: new Date(),
|
||||
notAfter: new Date(Date.now() + ttl * 1000)
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await sshCertificateBodyDAL.create(
|
||||
{
|
||||
sshCertId: cert.id,
|
||||
encryptedCertificate
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return { host, principals, serialNumber, signedPublicKey };
|
||||
};
|
||||
|
||||
const getSshHostUserCaPk = async (sshHostId: string) => {
|
||||
const host = await sshHostDAL.findById(sshHostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${sshHostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: host.projectId
|
||||
});
|
||||
|
||||
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
const getSshHostHostCaPk = async (sshHostId: string) => {
|
||||
const host = await sshHostDAL.findById(sshHostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
message: `SSH host with ID ${sshHostId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: host.projectId
|
||||
});
|
||||
|
||||
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
return {
|
||||
listSshHosts,
|
||||
createSshHost,
|
||||
updateSshHost,
|
||||
deleteSshHost,
|
||||
getSshHost,
|
||||
issueSshHostUserCert,
|
||||
issueSshHostHostCert,
|
||||
getSshHostUserCaPk,
|
||||
getSshHostHostCaPk
|
||||
};
|
||||
};
|
@@ -1,48 +0,0 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateSshHostDTO = {
|
||||
hostname: string;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
userSshCaId?: string;
|
||||
hostSshCaId?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateSshHostDTO = {
|
||||
sshHostId: string;
|
||||
hostname?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetSshHostDTO = {
|
||||
sshHostId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteSshHostDTO = {
|
||||
sshHostId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueSshHostUserCertDTO = {
|
||||
sshHostId: string;
|
||||
loginUser: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueSshHostHostCertDTO = {
|
||||
sshHostId: string;
|
||||
publicKey: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@@ -1,15 +0,0 @@
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
|
||||
export const isValidHostname = (value: string): boolean => {
|
||||
if (typeof value !== "string") return false;
|
||||
if (value.length > 255) return false;
|
||||
|
||||
// Only allow strict FQDNs, no wildcards or IPs
|
||||
return isFQDN(value, {
|
||||
require_tld: true,
|
||||
allow_underscores: false,
|
||||
allow_trailing_dot: false,
|
||||
allow_numeric_tld: true,
|
||||
allow_wildcard: false
|
||||
});
|
||||
};
|
@@ -1,10 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSshHostLoginUserDALFactory = ReturnType<typeof sshHostLoginUserDALFactory>;
|
||||
|
||||
export const sshHostLoginUserDALFactory = (db: TDbClient) => {
|
||||
const sshHostLoginUserOrm = ormify(db, TableName.SshHostLoginUser);
|
||||
return sshHostLoginUserOrm;
|
||||
};
|
@@ -1,31 +1,21 @@
|
||||
import { execFile } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import { promises as fs } from "fs";
|
||||
import { Knex } from "knex";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import {
|
||||
isValidHostPattern,
|
||||
isValidUserPattern
|
||||
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
||||
import {
|
||||
SshCaKeySource,
|
||||
SshCaStatus,
|
||||
SshCertType,
|
||||
TConvertActorToPrincipalsDTO,
|
||||
TCreateSshCaHelperDTO,
|
||||
TCreateSshCertDTO
|
||||
} from "./ssh-certificate-authority-types";
|
||||
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -41,35 +31,31 @@ export const createSshCertSerialNumber = () => {
|
||||
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
||||
* We use this function because the key format generated by `ssh-keygen` is unique.
|
||||
*/
|
||||
export const createSshKeyPair = async (keyAlgorithm: SshCertKeyAlgorithm) => {
|
||||
export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||
const privateKeyFile = path.join(tempDir, "id_key");
|
||||
const publicKeyFile = `${privateKeyFile}.pub`;
|
||||
|
||||
let keyType: string;
|
||||
let keyBits: string | null;
|
||||
let keyBits: string;
|
||||
|
||||
switch (keyAlgorithm) {
|
||||
case SshCertKeyAlgorithm.RSA_2048:
|
||||
case CertKeyAlgorithm.RSA_2048:
|
||||
keyType = "rsa";
|
||||
keyBits = "2048";
|
||||
break;
|
||||
case SshCertKeyAlgorithm.RSA_4096:
|
||||
case CertKeyAlgorithm.RSA_4096:
|
||||
keyType = "rsa";
|
||||
keyBits = "4096";
|
||||
break;
|
||||
case SshCertKeyAlgorithm.ECDSA_P256:
|
||||
case CertKeyAlgorithm.ECDSA_P256:
|
||||
keyType = "ecdsa";
|
||||
keyBits = "256";
|
||||
break;
|
||||
case SshCertKeyAlgorithm.ECDSA_P384:
|
||||
case CertKeyAlgorithm.ECDSA_P384:
|
||||
keyType = "ecdsa";
|
||||
keyBits = "384";
|
||||
break;
|
||||
case SshCertKeyAlgorithm.ED25519:
|
||||
keyType = "ed25519";
|
||||
keyBits = null;
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
||||
@@ -77,16 +63,10 @@ export const createSshKeyPair = async (keyAlgorithm: SshCertKeyAlgorithm) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const args = ["-t", keyType];
|
||||
if (keyBits !== null) {
|
||||
args.push("-b", keyBits);
|
||||
}
|
||||
args.push("-f", privateKeyFile, "-N", "");
|
||||
|
||||
// Generate the SSH key pair
|
||||
// The "-N ''" sets an empty passphrase
|
||||
// The keys are created in the temporary directory
|
||||
await execFileAsync("ssh-keygen", args, {
|
||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
|
||||
@@ -300,12 +280,7 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
||||
* that it only contains alphanumeric characters with no spaces.
|
||||
*/
|
||||
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||
const regex = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Colon,
|
||||
CharacterType.Period
|
||||
]);
|
||||
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
|
||||
if (!regex(keyId)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
@@ -347,96 +322,6 @@ const validateSshPublicKey = async (publicKey: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getKeyAlgorithmFromFingerprintOutput = (output: string): SshCertKeyAlgorithm | undefined => {
|
||||
const parts = output.trim().split(" ");
|
||||
const bitsInt = parseInt(parts[0], 10);
|
||||
const keyTypeRaw = parts.at(-1)?.replace(/[()]/g, ""); // remove surrounding parentheses
|
||||
|
||||
if (keyTypeRaw === "RSA") {
|
||||
return bitsInt === 2048 ? SshCertKeyAlgorithm.RSA_2048 : SshCertKeyAlgorithm.RSA_4096;
|
||||
}
|
||||
|
||||
if (keyTypeRaw === "ECDSA") {
|
||||
return bitsInt === 256 ? SshCertKeyAlgorithm.ECDSA_P256 : SshCertKeyAlgorithm.ECDSA_P384;
|
||||
}
|
||||
|
||||
if (keyTypeRaw === "ED25519") {
|
||||
return SshCertKeyAlgorithm.ED25519;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const normalizeSshPrivateKey = (raw: string): string => {
|
||||
return `${raw
|
||||
.replace(/\r\n/g, "\n") // Windows CRLF → LF
|
||||
.replace(/\r/g, "\n") // Old Mac CR → LF
|
||||
.replace(/\\n/g, "\n") // Double-escaped \n
|
||||
.trim()}\n`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the format of the SSH private key
|
||||
*
|
||||
* Returns the SSH public key corresponding to the private key
|
||||
* and the key algorithm categorization.
|
||||
*/
|
||||
export const validateSshPrivateKey = async (privateKey: string) => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-privkey-"));
|
||||
const privateKeyFile = path.join(tempDir, "id_key");
|
||||
|
||||
try {
|
||||
await fs.writeFile(privateKeyFile, privateKey, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600
|
||||
});
|
||||
|
||||
// This will fail if the private key is malformed or unreadable
|
||||
const { stdout: publicKey } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
|
||||
const { stdout: fingerprint } = await execFileAsync("ssh-keygen", ["-lf", privateKeyFile]);
|
||||
const keyAlgorithm = getKeyAlgorithmFromFingerprintOutput(fingerprint);
|
||||
|
||||
if (!keyAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SSH private key format: The key algorithm is not supported."
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
keyAlgorithm
|
||||
};
|
||||
} catch (err) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SSH private key format: could not be parsed."
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the provided public and private keys are valid and constitute
|
||||
* a matching SSH key pair.
|
||||
*/
|
||||
export const validateExternalSshCaKeyPair = async (publicKey: string, privateKey: string) => {
|
||||
await validateSshPublicKey(publicKey);
|
||||
|
||||
const { publicKey: derivedPublicKey, keyAlgorithm } = await validateSshPrivateKey(privateKey);
|
||||
|
||||
if (publicKey.trim() !== derivedPublicKey.trim()) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to validate matching SSH key pair: The provided public key does not match the public key derived from the private key."
|
||||
});
|
||||
}
|
||||
|
||||
return keyAlgorithm;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an SSH certificate for a user or host.
|
||||
*/
|
||||
@@ -446,32 +331,17 @@ export const createSshCert = async ({
|
||||
clientPublicKey,
|
||||
keyId,
|
||||
principals,
|
||||
requestedTtl, // in ms lib format
|
||||
requestedTtl,
|
||||
certType
|
||||
}: TCreateSshCertDTO) => {
|
||||
let ttl: number | undefined;
|
||||
// validate if the requested [certType] is allowed under the template configuration
|
||||
validateSshCertificateType(template, certType);
|
||||
|
||||
if (!template && requestedTtl) {
|
||||
const parsedTtl = Math.ceil(ms(requestedTtl) / 1000);
|
||||
if (parsedTtl > 0) ttl = parsedTtl;
|
||||
}
|
||||
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
||||
validateSshCertificatePrincipals(certType, template, principals);
|
||||
|
||||
if (template) {
|
||||
// validate if the requested [certType] is allowed under the template configuration
|
||||
validateSshCertificateType(template, certType);
|
||||
|
||||
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
||||
validateSshCertificatePrincipals(certType, template, principals);
|
||||
|
||||
// validate if the requested TTL is valid under the template configuration
|
||||
ttl = validateSshCertificateTtl(template, requestedTtl);
|
||||
}
|
||||
|
||||
if (!ttl) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create SSH certificate due to missing TTL"
|
||||
});
|
||||
}
|
||||
// validate if the requested TTL is valid under the template configuration
|
||||
const ttl = validateSshCertificateTtl(template, requestedTtl);
|
||||
|
||||
validateSshCertificateKeyId(keyId);
|
||||
await validateSshPublicKey(clientPublicKey);
|
||||
@@ -518,88 +388,3 @@ export const createSshCert = async ({
|
||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
export const createSshCaHelper = async ({
|
||||
projectId,
|
||||
friendlyName,
|
||||
keyAlgorithm: requestedKeyAlgorithm,
|
||||
keySource,
|
||||
externalPk,
|
||||
externalSk,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
kmsService,
|
||||
tx: outerTx
|
||||
}: TCreateSshCaHelperDTO) => {
|
||||
// Function to handle the actual creation logic
|
||||
const processCreation = async (tx: Knex) => {
|
||||
let publicKey: string;
|
||||
let privateKey: string;
|
||||
let keyAlgorithm: SshCertKeyAlgorithm = requestedKeyAlgorithm;
|
||||
if (keySource === SshCaKeySource.INTERNAL) {
|
||||
// generate SSH CA key pair internally
|
||||
({ publicKey, privateKey } = await createSshKeyPair(requestedKeyAlgorithm));
|
||||
} else {
|
||||
// use external SSH CA key pair
|
||||
if (!externalPk || !externalSk) {
|
||||
throw new BadRequestError({
|
||||
message: "Public and private keys are required when key source is external"
|
||||
});
|
||||
}
|
||||
publicKey = externalPk;
|
||||
privateKey = externalSk;
|
||||
keyAlgorithm = await validateExternalSshCaKeyPair(publicKey, privateKey);
|
||||
}
|
||||
const ca = await sshCertificateAuthorityDAL.create(
|
||||
{
|
||||
projectId,
|
||||
friendlyName,
|
||||
status: SshCaStatus.ACTIVE,
|
||||
keyAlgorithm,
|
||||
keySource
|
||||
},
|
||||
tx
|
||||
);
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey(
|
||||
{
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
await sshCertificateAuthoritySecretDAL.create(
|
||||
{
|
||||
sshCaId: ca.id,
|
||||
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { ...ca, publicKey };
|
||||
};
|
||||
|
||||
if (outerTx) {
|
||||
return processCreation(outerTx);
|
||||
}
|
||||
|
||||
return sshCertificateAuthorityDAL.transaction(processCreation);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert an actor to a list of principals to be included in an SSH certificate.
|
||||
*
|
||||
* (dangtony98): This function is only supported for user actors at the moment and returns
|
||||
* only the email of the associated user. In the future, we will consider other
|
||||
* actor types and attributes such as group membership slugs and/or metadata to be
|
||||
* included in the list of principals.
|
||||
*/
|
||||
export const convertActorToPrincipals = async ({ userDAL, actor, actorId }: TConvertActorToPrincipalsDTO) => {
|
||||
if (actor !== ActorType.USER) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to convert actor to principals due to unsupported actor type"
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
return [user.username];
|
||||
};
|
||||
|
@@ -5,6 +5,5 @@ export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({
|
||||
projectId: true,
|
||||
friendlyName: true,
|
||||
status: true,
|
||||
keyAlgorithm: true,
|
||||
keySource: true
|
||||
keyAlgorithm: true
|
||||
});
|
||||
|
@@ -13,7 +13,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { createSshCaHelper, createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
||||
import { createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
||||
import {
|
||||
SshCaStatus,
|
||||
TCreateSshCaDTO,
|
||||
@@ -59,10 +59,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
||||
const createSshCa = async ({
|
||||
projectId,
|
||||
friendlyName,
|
||||
keyAlgorithm: requestedKeyAlgorithm,
|
||||
publicKey: externalPk,
|
||||
privateKey: externalSk,
|
||||
keySource,
|
||||
keyAlgorithm,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
@@ -82,16 +79,33 @@ export const sshCertificateAuthorityServiceFactory = ({
|
||||
ProjectPermissionSub.SshCertificateAuthorities
|
||||
);
|
||||
|
||||
const newCa = await createSshCaHelper({
|
||||
projectId,
|
||||
friendlyName,
|
||||
keyAlgorithm: requestedKeyAlgorithm,
|
||||
keySource,
|
||||
externalPk,
|
||||
externalSk,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
kmsService
|
||||
const newCa = await sshCertificateAuthorityDAL.transaction(async (tx) => {
|
||||
const ca = await sshCertificateAuthorityDAL.create(
|
||||
{
|
||||
projectId,
|
||||
friendlyName,
|
||||
status: SshCaStatus.ACTIVE,
|
||||
keyAlgorithm
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
await sshCertificateAuthoritySecretDAL.create(
|
||||
{
|
||||
sshCaId: ca.id,
|
||||
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return { ...ca, publicKey };
|
||||
});
|
||||
|
||||
return newCa;
|
||||
|
@@ -1,24 +1,12 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
export enum SshCaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled"
|
||||
}
|
||||
|
||||
export enum SshCaKeySource {
|
||||
INTERNAL = "internal",
|
||||
EXTERNAL = "external"
|
||||
}
|
||||
|
||||
export enum SshCertType {
|
||||
USER = "user",
|
||||
HOST = "host"
|
||||
@@ -26,25 +14,9 @@ export enum SshCertType {
|
||||
|
||||
export type TCreateSshCaDTO = {
|
||||
friendlyName: string;
|
||||
keyAlgorithm: SshCertKeyAlgorithm;
|
||||
publicKey?: string;
|
||||
privateKey?: string;
|
||||
keySource: SshCaKeySource;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TCreateSshCaHelperDTO = {
|
||||
projectId: string;
|
||||
friendlyName: string;
|
||||
keyAlgorithm: SshCertKeyAlgorithm;
|
||||
keySource: SshCaKeySource;
|
||||
externalPk?: string;
|
||||
externalSk?: string;
|
||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "transaction" | "create">;
|
||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
export type TGetSshCaDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@@ -65,7 +37,7 @@ export type TDeleteSshCaDTO = {
|
||||
|
||||
export type TIssueSshCredsDTO = {
|
||||
certificateTemplateId: string;
|
||||
keyAlgorithm: SshCertKeyAlgorithm;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
certType: SshCertType;
|
||||
principals: string[];
|
||||
ttl?: string;
|
||||
@@ -86,7 +58,7 @@ export type TGetSshCaCertificateTemplatesDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateSshCertDTO = {
|
||||
template?: TSshCertificateTemplates;
|
||||
template: TSshCertificateTemplates;
|
||||
caPrivateKey: string;
|
||||
clientPublicKey: string;
|
||||
keyId: string;
|
||||
@@ -94,9 +66,3 @@ export type TCreateSshCertDTO = {
|
||||
requestedTtl?: string;
|
||||
certType: SshCertType;
|
||||
};
|
||||
|
||||
export type TConvertActorToPrincipalsDTO = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
};
|
||||
|
@@ -77,8 +77,6 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
|
||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||
|
||||
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
||||
|
||||
const waitTillReady = async ({
|
||||
key,
|
||||
waitingCb,
|
||||
@@ -105,7 +103,6 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
return {
|
||||
setItem,
|
||||
getItem,
|
||||
setExpiry,
|
||||
setItemWithExpiry,
|
||||
deleteItem,
|
||||
incrementBy,
|
||||
|
@@ -10,7 +10,6 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
||||
store[key] = value;
|
||||
return "OK";
|
||||
},
|
||||
setExpiry: async () => 0,
|
||||
setItemWithExpiry: async (key, value) => {
|
||||
store[key] = value;
|
||||
return "OK";
|
||||
|
@@ -519,9 +519,6 @@ export const PROJECTS = {
|
||||
LIST_SSH_CAS: {
|
||||
projectId: "The ID of the project to list SSH CAs for."
|
||||
},
|
||||
LIST_SSH_HOSTS: {
|
||||
projectId: "The ID of the project to list SSH hosts for."
|
||||
},
|
||||
LIST_SSH_CERTIFICATES: {
|
||||
projectId: "The ID of the project to list SSH certificates for.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
||||
@@ -1256,11 +1253,7 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the SSH CA in.",
|
||||
friendlyName: "A friendly name for the SSH CA.",
|
||||
keyAlgorithm:
|
||||
"The type of public key algorithm and size, in bits, of the key pair for the SSH CA; required if keySource is internal.",
|
||||
publicKey: "The public key for the SSH CA key pair; required if keySource is external.",
|
||||
privateKey: "The private key for the SSH CA key pair; required if keySource is external.",
|
||||
keySource: "The source of the SSH CA key pair. This can be one of internal or external."
|
||||
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA."
|
||||
},
|
||||
GET: {
|
||||
sshCaId: "The ID of the SSH CA to get."
|
||||
@@ -1334,62 +1327,6 @@ export const SSH_CERTIFICATE_TEMPLATES = {
|
||||
}
|
||||
};
|
||||
|
||||
export const SSH_HOSTS = {
|
||||
GET: {
|
||||
sshHostId: "The ID of the SSH host to get."
|
||||
},
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the SSH host in.",
|
||||
hostname: "The hostname of the SSH host.",
|
||||
userCertTtl: "The time to live for user certificates issued under this host.",
|
||||
hostCertTtl: "The time to live for host certificates issued under this host.",
|
||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||
loginMappings:
|
||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project.",
|
||||
userSshCaId:
|
||||
"The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.",
|
||||
hostSshCaId:
|
||||
"The ID of the SSH CA to use for host certificates. If not specified, the default host SSH CA will be used if it exists."
|
||||
},
|
||||
UPDATE: {
|
||||
sshHostId: "The ID of the SSH host to update.",
|
||||
hostname: "The hostname of the SSH host to update to.",
|
||||
userCertTtl: "The time to live for user certificates issued under this host to update to.",
|
||||
hostCertTtl: "The time to live for host certificates issued under this host to update to.",
|
||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||
loginMappings:
|
||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
|
||||
},
|
||||
DELETE: {
|
||||
sshHostId: "The ID of the SSH host to delete."
|
||||
},
|
||||
ISSUE_SSH_CREDENTIALS: {
|
||||
sshHostId: "The ID of the SSH host to issue the SSH credentials for.",
|
||||
loginUser: "The login user to issue the SSH credentials for.",
|
||||
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH host.",
|
||||
serialNumber: "The serial number of the issued SSH certificate.",
|
||||
signedKey: "The SSH certificate or signed SSH public key.",
|
||||
privateKey: "The private key corresponding to the issued SSH certificate.",
|
||||
publicKey: "The public key of the issued SSH certificate."
|
||||
},
|
||||
ISSUE_HOST_CERT: {
|
||||
sshHostId: "The ID of the SSH host to issue the SSH certificate for.",
|
||||
publicKey: "The SSH public key to issue the SSH certificate for.",
|
||||
serialNumber: "The serial number of the issued SSH certificate.",
|
||||
signedKey: "The SSH certificate or signed SSH public key."
|
||||
},
|
||||
GET_USER_CA_PUBLIC_KEY: {
|
||||
sshHostId: "The ID of the SSH host to get the user SSH CA public key for.",
|
||||
publicKey: "The public key of the user SSH CA linked to the SSH host."
|
||||
},
|
||||
GET_HOST_CA_PUBLIC_KEY: {
|
||||
sshHostId: "The ID of the SSH host to get the host SSH CA public key for.",
|
||||
publicKey: "The public key of the host SSH CA linked to the SSH host."
|
||||
}
|
||||
};
|
||||
|
||||
export const CERTIFICATE_AUTHORITIES = {
|
||||
CREATE: {
|
||||
projectSlug: "Slug of the project to create the CA in.",
|
||||
@@ -1672,8 +1609,7 @@ export const KMS = {
|
||||
projectId: "The ID of the project to create the key in.",
|
||||
name: "The name of the key to be created. Must be slug-friendly.",
|
||||
description: "An optional description of the key.",
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.",
|
||||
type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key."
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||
},
|
||||
UPDATE_KEY: {
|
||||
keyId: "The ID of the key to be updated.",
|
||||
@@ -1706,28 +1642,6 @@ export const KMS = {
|
||||
DECRYPT: {
|
||||
keyId: "The ID of the key to decrypt the data with.",
|
||||
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
||||
},
|
||||
|
||||
LIST_SIGNING_ALGORITHMS: {
|
||||
keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying."
|
||||
},
|
||||
|
||||
GET_PUBLIC_KEY: {
|
||||
keyId: "The ID of the key to get the public key for. The key must be for signing and verifying."
|
||||
},
|
||||
|
||||
SIGN: {
|
||||
keyId: "The ID of the key to sign the data with.",
|
||||
data: "The data in string format to be signed (base64 encoded).",
|
||||
isDigest:
|
||||
"Whether the data is already digested or not. Please be aware that if you are passing a digest the algorithm used to create the digest must match the signing algorithm used to sign the digest.",
|
||||
signingAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||
},
|
||||
VERIFY: {
|
||||
keyId: "The ID of the key to verify the data with.",
|
||||
data: "The data in string format to be verified (base64 encoded). For data larger than 4096 bytes you must first create a digest of the data and then pass the digest in the data parameter.",
|
||||
signature: "The signature to be verified (base64 encoded).",
|
||||
isDigest: "Whether the data is already digested or not."
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1791,16 +1705,6 @@ export const AppConnections = {
|
||||
sslEnabled: "Whether or not to use SSL when connecting to the database.",
|
||||
sslRejectUnauthorized: "Whether or not to reject unauthorized SSL certificates.",
|
||||
sslCertificate: "The SSL certificate to use for connection."
|
||||
},
|
||||
TERRAFORM_CLOUD: {
|
||||
apiToken: "The API token to use to connect with Terraform Cloud."
|
||||
},
|
||||
VERCEL: {
|
||||
apiToken: "The API token used to authenticate with Vercel."
|
||||
},
|
||||
CAMUNDA: {
|
||||
clientId: "The client ID used to authenticate with Camunda.",
|
||||
clientSecret: "The client secret used to authenticate with Camunda."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1911,31 +1815,11 @@ export const SecretSyncs = {
|
||||
DATABRICKS: {
|
||||
scope: "The Databricks secret scope that secrets should be synced to."
|
||||
},
|
||||
CAMUNDA: {
|
||||
scope: "The Camunda scope that secrets should be synced to.",
|
||||
clusterUUID: "The UUID of the Camunda cluster that secrets should be synced to."
|
||||
},
|
||||
HUMANITEC: {
|
||||
app: "The ID of the Humanitec app to sync secrets to.",
|
||||
org: "The ID of the Humanitec org to sync secrets to.",
|
||||
env: "The ID of the Humanitec environment to sync secrets to.",
|
||||
scope: "The Humanitec scope that secrets should be synced to."
|
||||
},
|
||||
TERRAFORM_CLOUD: {
|
||||
org: "The ID of the Terraform Cloud org to sync secrets to.",
|
||||
variableSetName: "The name of the Terraform Cloud Variable Set to sync secrets to.",
|
||||
variableSetId: "The ID of the Terraform Cloud Variable Set to sync secrets to.",
|
||||
workspaceName: "The name of the Terraform Cloud workspace to sync secrets to.",
|
||||
workspaceId: "The ID of the Terraform Cloud workspace to sync secrets to.",
|
||||
scope: "The Terraform Cloud scope that secrets should be synced to.",
|
||||
category: "The Terraform Cloud category that secrets should be synced to."
|
||||
},
|
||||
VERCEL: {
|
||||
app: "The ID of the Vercel app to sync secrets to.",
|
||||
appName: "The name of the Vercel app to sync secrets to.",
|
||||
env: "The ID of the Vercel environment to sync secrets to.",
|
||||
branch: "The branch to sync preview secrets to.",
|
||||
teamId: "The ID of the Vercel team to sync secrets to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -24,5 +24,6 @@ export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$EQ = "$eq",
|
||||
$NEQ = "$ne",
|
||||
$GLOB = "$glob"
|
||||
$GLOB = "$glob",
|
||||
$ELEMENTMATCH = "$elemMatch"
|
||||
}
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export const generateCacheKeyFromData = (data: unknown) =>
|
||||
crypto
|
||||
.createHash("md5")
|
||||
.update(JSON.stringify(data))
|
||||
.digest("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types";
|
||||
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
||||
|
||||
const getIvLength = () => {
|
||||
return 12;
|
||||
@@ -10,9 +10,7 @@ const getTagLength = () => {
|
||||
return 16;
|
||||
};
|
||||
|
||||
export const symmetricCipherService = (
|
||||
type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256
|
||||
): TSymmetricEncryptionFns => {
|
||||
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
|
||||
const IV_LENGTH = getIvLength();
|
||||
const TAG_LENGTH = getTagLength();
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
export { symmetricCipherService } from "./cipher";
|
||||
export { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "./types";
|
||||
export { SymmetricEncryption } from "./types";
|
||||
|
@@ -1,18 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AsymmetricKeyAlgorithm } from "../sign/types";
|
||||
|
||||
// Supported symmetric encrypt/decrypt algorithms
|
||||
export enum SymmetricKeyAlgorithm {
|
||||
export enum SymmetricEncryption {
|
||||
AES_GCM_256 = "aes-256-gcm",
|
||||
AES_GCM_128 = "aes-128-gcm"
|
||||
}
|
||||
export const SymmetricKeyAlgorithmEnum = z.enum(Object.values(SymmetricKeyAlgorithm) as [string, ...string[]]).options;
|
||||
|
||||
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||
...Object.values(SymmetricKeyAlgorithm),
|
||||
...Object.values(AsymmetricKeyAlgorithm)
|
||||
] as [string, ...string[]]).options;
|
||||
|
||||
export type TSymmetricEncryptionFns = {
|
||||
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||
|
@@ -1,2 +0,0 @@
|
||||
export { signingService } from "./signing";
|
||||
export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types";
|
@@ -1,539 +0,0 @@
|
||||
import { execFile } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { cleanTemporaryDirectory, createTemporaryDirectory, writeToTemporaryFile } from "@app/lib/files";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
interface SigningParams {
|
||||
hashAlgorithm: SupportedHashAlgorithm;
|
||||
padding?: number;
|
||||
saltLength?: number;
|
||||
}
|
||||
|
||||
enum SupportedHashAlgorithm {
|
||||
SHA256 = "sha256",
|
||||
SHA384 = "sha384",
|
||||
SHA512 = "sha512"
|
||||
}
|
||||
|
||||
const COMMAND_TIMEOUT = 15_000;
|
||||
|
||||
const SHA256_DIGEST_LENGTH = 32;
|
||||
const SHA384_DIGEST_LENGTH = 48;
|
||||
const SHA512_DIGEST_LENGTH = 64;
|
||||
|
||||
/**
|
||||
* Service for cryptographic signing and verification operations using asymmetric keys
|
||||
*
|
||||
* @param algorithm The key algorithm itself. The signing algorithm is supplied in the individual sign/verify functions.
|
||||
* @returns Object with sign and verify functions
|
||||
*/
|
||||
export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSignVerifyFns => {
|
||||
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
|
||||
switch (signingAlgorithm) {
|
||||
// RSA PSS
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_512:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA512_DIGEST_LENGTH
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_256:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA256_DIGEST_LENGTH
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_384:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA384_DIGEST_LENGTH
|
||||
};
|
||||
|
||||
// RSA PKCS#1 v1.5
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
|
||||
// ECDSA
|
||||
case SigningAlgorithm.ECDSA_SHA_256:
|
||||
return { hashAlgorithm: SupportedHashAlgorithm.SHA256 };
|
||||
case SigningAlgorithm.ECDSA_SHA_384:
|
||||
return { hashAlgorithm: SupportedHashAlgorithm.SHA384 };
|
||||
case SigningAlgorithm.ECDSA_SHA_512:
|
||||
return { hashAlgorithm: SupportedHashAlgorithm.SHA512 };
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`);
|
||||
}
|
||||
};
|
||||
|
||||
const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => {
|
||||
// We will support more in the future
|
||||
switch (keyAlgorithm) {
|
||||
case AsymmetricKeyAlgorithm.ECC_NIST_P256:
|
||||
return {
|
||||
full: "prime256v1",
|
||||
short: "p256"
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported EC curve: ${keyAlgorithm}`);
|
||||
}
|
||||
};
|
||||
|
||||
const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => {
|
||||
const isRsaKey = algorithm.startsWith("RSA");
|
||||
const isEccKey = algorithm.startsWith("ECC");
|
||||
|
||||
const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA");
|
||||
const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA");
|
||||
|
||||
if (isRsaKey && !isRsaAlgorithm) {
|
||||
throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` });
|
||||
}
|
||||
|
||||
if (isEccKey && !isEccAlgorithm) {
|
||||
throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` });
|
||||
}
|
||||
};
|
||||
|
||||
const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
||||
const digestPath = path.join(tempDir, "digest.bin");
|
||||
const sigPath = path.join(tempDir, "signature.bin");
|
||||
const keyPath = path.join(tempDir, "key.pem");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(digestPath, digest);
|
||||
await writeToTemporaryFile(keyPath, privateKey);
|
||||
|
||||
const { stderr } = await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-sign",
|
||||
"-in",
|
||||
digestPath,
|
||||
"-inkey",
|
||||
keyPath,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`,
|
||||
"-out",
|
||||
sigPath
|
||||
],
|
||||
{
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: COMMAND_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
logger.error(stderr, "KMS: Failed to sign RSA digest");
|
||||
throw new BadRequestError({
|
||||
message: "Failed to sign RSA digest due to signing error"
|
||||
});
|
||||
}
|
||||
const signature = await fs.readFile(sigPath);
|
||||
|
||||
if (!signature) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||
});
|
||||
}
|
||||
|
||||
return signature;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||
const tempDir = await createTemporaryDirectory("ecc-sign");
|
||||
const digestPath = path.join(tempDir, "digest.bin");
|
||||
const keyPath = path.join(tempDir, "key.pem");
|
||||
const sigPath = path.join(tempDir, "signature.bin");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(digestPath, digest);
|
||||
await writeToTemporaryFile(keyPath, privateKey);
|
||||
|
||||
const { stderr } = await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-sign",
|
||||
"-in",
|
||||
digestPath,
|
||||
"-inkey",
|
||||
keyPath,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`,
|
||||
"-out",
|
||||
sigPath
|
||||
],
|
||||
{
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: COMMAND_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
logger.error(stderr, "KMS: Failed to sign ECC digest");
|
||||
throw new BadRequestError({
|
||||
message: "Failed to sign ECC digest due to signing error"
|
||||
});
|
||||
}
|
||||
|
||||
const signature = await fs.readFile(sigPath);
|
||||
|
||||
if (!signature) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||
});
|
||||
}
|
||||
|
||||
return signature;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const $verifyEccDigest = async (
|
||||
digest: Buffer,
|
||||
signature: Buffer,
|
||||
publicKey: Buffer,
|
||||
hashAlgorithm: SupportedHashAlgorithm
|
||||
) => {
|
||||
const tempDir = await createTemporaryDirectory("ecc-signature-verification");
|
||||
const publicKeyFile = path.join(tempDir, "public-key.pem");
|
||||
const sigFile = path.join(tempDir, "signature.sig");
|
||||
const digestFile = path.join(tempDir, "digest.bin");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||
await writeToTemporaryFile(sigFile, signature);
|
||||
await writeToTemporaryFile(digestFile, digest);
|
||||
|
||||
await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-verify",
|
||||
"-in",
|
||||
digestFile,
|
||||
"-inkey",
|
||||
publicKeyFile,
|
||||
"-pubin", // Important for EC public keys
|
||||
"-sigfile",
|
||||
sigFile,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`
|
||||
],
|
||||
{ timeout: COMMAND_TIMEOUT }
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as { stderr: string };
|
||||
|
||||
if (
|
||||
!err?.stderr?.toLowerCase()?.includes("signature verification failure") &&
|
||||
!err?.stderr?.toLowerCase()?.includes("bad signature")
|
||||
) {
|
||||
logger.error(error, "KMS: Failed to verify ECC signature");
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const $verifyRsaDigest = async (
|
||||
digest: Buffer,
|
||||
signature: Buffer,
|
||||
publicKey: Buffer,
|
||||
hashAlgorithm: SupportedHashAlgorithm
|
||||
) => {
|
||||
const tempDir = await createTemporaryDirectory("kms-signature-verification");
|
||||
const publicKeyFile = path.join(tempDir, "public-key.pub");
|
||||
const signatureFile = path.join(tempDir, "signature.sig");
|
||||
const digestFile = path.join(tempDir, "digest.bin");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||
await writeToTemporaryFile(signatureFile, signature);
|
||||
await writeToTemporaryFile(digestFile, digest);
|
||||
|
||||
await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-verify",
|
||||
"-in",
|
||||
digestFile,
|
||||
"-inkey",
|
||||
publicKeyFile,
|
||||
"-pubin",
|
||||
"-sigfile",
|
||||
signatureFile,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`
|
||||
],
|
||||
{ timeout: COMMAND_TIMEOUT }
|
||||
);
|
||||
|
||||
// it'll throw if the verification was not successful
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as { stdout: string };
|
||||
|
||||
if (!err?.stdout?.toLowerCase()?.includes("signature verification failure")) {
|
||||
logger.error(error, "KMS: Failed to verify signature");
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyDigestFunctionsMap: Record<
|
||||
AsymmetricKeyAlgorithm,
|
||||
(data: Buffer, signature: Buffer, publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<boolean>
|
||||
> = {
|
||||
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $verifyEccDigest,
|
||||
[AsymmetricKeyAlgorithm.RSA_4096]: $verifyRsaDigest
|
||||
};
|
||||
|
||||
const signDigestFunctionsMap: Record<
|
||||
AsymmetricKeyAlgorithm,
|
||||
(data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<Buffer>
|
||||
> = {
|
||||
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
|
||||
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
||||
};
|
||||
|
||||
const sign = async (
|
||||
data: Buffer,
|
||||
privateKey: Buffer,
|
||||
signingAlgorithm: SigningAlgorithm,
|
||||
isDigest: boolean
|
||||
): Promise<Buffer> => {
|
||||
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||
|
||||
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||
|
||||
if (isDigest) {
|
||||
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||
throw new BadRequestError({
|
||||
message: "RSA PSS does not support digested input"
|
||||
});
|
||||
}
|
||||
|
||||
const signFunction = signDigestFunctionsMap[algorithm];
|
||||
|
||||
if (!signFunction) {
|
||||
throw new BadRequestError({
|
||||
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||
});
|
||||
}
|
||||
|
||||
const signature = await signFunction(data, privateKey, hashAlgorithm);
|
||||
return signature;
|
||||
}
|
||||
|
||||
const privateKeyObject = crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: "pem",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
// For RSA signatures
|
||||
if (signingAlgorithm.startsWith("RSA")) {
|
||||
const signer = crypto.createSign(hashAlgorithm);
|
||||
signer.update(data);
|
||||
|
||||
return signer.sign({
|
||||
key: privateKeyObject,
|
||||
padding,
|
||||
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||
});
|
||||
}
|
||||
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||
// For ECDSA signatures
|
||||
const signer = crypto.createSign(hashAlgorithm);
|
||||
signer.update(data);
|
||||
return signer.sign({
|
||||
key: privateKeyObject,
|
||||
dsaEncoding: "der"
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Signing algorithm ${signingAlgorithm} not implemented`
|
||||
});
|
||||
};
|
||||
|
||||
const verify = async (
|
||||
data: Buffer,
|
||||
signature: Buffer,
|
||||
publicKey: Buffer,
|
||||
signingAlgorithm: SigningAlgorithm,
|
||||
isDigest: boolean
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||
|
||||
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||
|
||||
if (isDigest) {
|
||||
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||
throw new BadRequestError({
|
||||
message: "RSA PSS does not support digested input"
|
||||
});
|
||||
}
|
||||
|
||||
const verifyFunction = verifyDigestFunctionsMap[algorithm];
|
||||
|
||||
if (!verifyFunction) {
|
||||
throw new BadRequestError({
|
||||
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||
});
|
||||
}
|
||||
|
||||
const signatureValid = await verifyFunction(data, signature, publicKey, hashAlgorithm);
|
||||
|
||||
return signatureValid;
|
||||
}
|
||||
|
||||
const publicKeyObject = crypto.createPublicKey({
|
||||
key: publicKey,
|
||||
format: "der",
|
||||
type: "spki"
|
||||
});
|
||||
|
||||
// For RSA signatures
|
||||
if (signingAlgorithm.startsWith("RSA")) {
|
||||
const verifier = crypto.createVerify(hashAlgorithm);
|
||||
verifier.update(data);
|
||||
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKeyObject,
|
||||
padding,
|
||||
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||
},
|
||||
signature
|
||||
);
|
||||
}
|
||||
// For ECDSA signatures
|
||||
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||
const verifier = crypto.createVerify(hashAlgorithm);
|
||||
verifier.update(data);
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKeyObject,
|
||||
dsaEncoding: "der"
|
||||
},
|
||||
signature
|
||||
);
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Verification for algorithm ${signingAlgorithm} not implemented`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error(error, "KMS: Failed to verify signature");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const generateAsymmetricPrivateKey = async () => {
|
||||
const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => {
|
||||
if (algorithm.startsWith("RSA")) {
|
||||
crypto.generateKeyPair(
|
||||
"rsa",
|
||||
{
|
||||
modulusLength: Number(algorithm.split("_")[1]),
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||
},
|
||||
(err, _, pk) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ privateKey: pk });
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const { full: namedCurve } = $getEcCurveName(algorithm);
|
||||
|
||||
crypto.generateKeyPair(
|
||||
"ec",
|
||||
{
|
||||
namedCurve,
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||
},
|
||||
(err, _, pk) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({
|
||||
privateKey: pk
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Buffer.from(privateKey);
|
||||
};
|
||||
|
||||
const getPublicKeyFromPrivateKey = (privateKey: Buffer) => {
|
||||
const privateKeyObj = crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: "pem",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
const publicKey = crypto.createPublicKey(privateKeyObj).export({
|
||||
type: "spki",
|
||||
format: "der"
|
||||
});
|
||||
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
return {
|
||||
sign,
|
||||
verify,
|
||||
generateAsymmetricPrivateKey,
|
||||
getPublicKeyFromPrivateKey
|
||||
};
|
||||
};
|
@@ -1,45 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type TAsymmetricSignVerifyFns = {
|
||||
sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm, isDigest: boolean) => Promise<Buffer>;
|
||||
verify: (
|
||||
data: Buffer,
|
||||
signature: Buffer,
|
||||
key: Buffer,
|
||||
signingAlgorithm: SigningAlgorithm,
|
||||
isDigest: boolean
|
||||
) => Promise<boolean>;
|
||||
generateAsymmetricPrivateKey: () => Promise<Buffer>;
|
||||
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
|
||||
};
|
||||
|
||||
// Supported asymmetric key types
|
||||
export enum AsymmetricKeyAlgorithm {
|
||||
RSA_4096 = "RSA_4096",
|
||||
ECC_NIST_P256 = "ECC_NIST_P256"
|
||||
}
|
||||
|
||||
export const AsymmetricKeyAlgorithmEnum = z.enum(
|
||||
Object.values(AsymmetricKeyAlgorithm) as [string, ...string[]]
|
||||
).options;
|
||||
|
||||
export enum SigningAlgorithm {
|
||||
// RSA PSS algorithms
|
||||
// These are NOT deterministic and include randomness.
|
||||
// This means that the output signature is different each time for the same input.
|
||||
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
|
||||
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
|
||||
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
|
||||
|
||||
// RSA PKCS#1 v1.5 algorithms
|
||||
// These are deterministic and the output is the same each time for the same input.
|
||||
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
|
||||
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
|
||||
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
|
||||
|
||||
// ECDSA algorithms
|
||||
// None of these are deterministic and include randomness like RSA PSS.
|
||||
ECDSA_SHA_512 = "ECDSA_SHA_512",
|
||||
ECDSA_SHA_384 = "ECDSA_SHA_384",
|
||||
ECDSA_SHA_256 = "ECDSA_SHA_256"
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
const baseDir = path.join(os.tmpdir(), "infisical");
|
||||
const randomPath = () => `${crypto.randomBytes(32).toString("hex")}`;
|
||||
|
||||
export const createTemporaryDirectory = async (name: string) => {
|
||||
const tempDirPath = path.join(baseDir, `${name}-${randomPath()}`);
|
||||
await fs.mkdir(tempDirPath, { recursive: true });
|
||||
|
||||
return tempDirPath;
|
||||
};
|
||||
|
||||
export const removeTemporaryBaseDirectory = async () => {
|
||||
await fs.rm(baseDir, { force: true, recursive: true }).catch((err) => {
|
||||
logger.error(err, `Failed to remove temporary base directory [path=${baseDir}]`);
|
||||
});
|
||||
};
|
||||
|
||||
export const cleanTemporaryDirectory = async (dirPath: string) => {
|
||||
await fs.rm(dirPath, { recursive: true, force: true }).catch((err) => {
|
||||
logger.error(err, `Failed to cleanup temporary directory [path=${dirPath}]`);
|
||||
});
|
||||
};
|
||||
|
||||
export const writeToTemporaryFile = async (tempDirPath: string, data: string | Buffer) => {
|
||||
await fs.writeFile(tempDirPath, data, { mode: 0o600 }).catch((err) => {
|
||||
logger.error(err, `Failed to write to temporary file [path=${tempDirPath}]`);
|
||||
throw err;
|
||||
});
|
||||
};
|
@@ -1 +0,0 @@
|
||||
export * from "./files";
|
@@ -41,18 +41,6 @@ export type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
export type BufferKeysToString<T> = {
|
||||
[K in keyof T]: T[K] extends Buffer
|
||||
? string
|
||||
: T[K] extends Buffer | null
|
||||
? string | null
|
||||
: T[K] extends Buffer | undefined
|
||||
? string | undefined
|
||||
: T[K] extends Buffer | null | undefined
|
||||
? string | null | undefined
|
||||
: T[K];
|
||||
};
|
||||
|
||||
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||
|
||||
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
||||
|
@@ -9,7 +9,6 @@ import { runMigrations } from "./auto-start-migrations";
|
||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||
import { keyStoreFactory } from "./keystore/keystore";
|
||||
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
||||
import { removeTemporaryBaseDirectory } from "./lib/files";
|
||||
import { initLogger } from "./lib/logger";
|
||||
import { queueServiceFactory } from "./queue";
|
||||
import { main } from "./server/app";
|
||||
@@ -22,8 +21,6 @@ const run = async () => {
|
||||
const logger = initLogger();
|
||||
const envConfig = initEnvConfig(logger);
|
||||
|
||||
await removeTemporaryBaseDirectory();
|
||||
|
||||
const db = initDbConnection({
|
||||
dbConnectionUri: envConfig.DB_CONNECTION_URI,
|
||||
dbRootCert: envConfig.DB_ROOT_CERT,
|
||||
@@ -74,7 +71,6 @@ const run = async () => {
|
||||
process.on("SIGINT", async () => {
|
||||
await server.close();
|
||||
await db.destroy();
|
||||
await removeTemporaryBaseDirectory();
|
||||
hsmModule.finalize();
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -83,7 +79,6 @@ const run = async () => {
|
||||
process.on("SIGTERM", async () => {
|
||||
await server.close();
|
||||
await db.destroy();
|
||||
await removeTemporaryBaseDirectory();
|
||||
hsmModule.finalize();
|
||||
process.exit(0);
|
||||
});
|
||||
|
@@ -93,10 +93,3 @@ export const userEngagementLimit: RateLimitOptions = {
|
||||
max: 5,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const publicSshCaLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
hook: "preValidation",
|
||||
max: 30, // conservative default
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
@@ -45,6 +45,4 @@ export const BaseSecretNameSchema = z.string().trim().min(1);
|
||||
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||
(el) => !el.includes(" "),
|
||||
"Secret name cannot contain spaces."
|
||||
)
|
||||
.refine((el) => !el.includes(":"), "Secret name cannot contain colon.")
|
||||
.refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
|
||||
).refine((el) => !el.includes(":"), "Secret name cannot contain colon.");
|
||||
|
@@ -96,10 +96,6 @@ import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/s
|
||||
import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||
import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||
import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||
import { sshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||
import { sshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||
import { sshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||
import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
@@ -188,7 +184,6 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||
import { projectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
@@ -297,7 +292,6 @@ export const registerRoutes = async (
|
||||
const apiKeyDAL = apiKeyDALFactory(db);
|
||||
|
||||
const projectDAL = projectDALFactory(db);
|
||||
const projectSshConfigDAL = projectSshConfigDALFactory(db);
|
||||
const projectMembershipDAL = projectMembershipDALFactory(db);
|
||||
const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db);
|
||||
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
|
||||
@@ -315,7 +309,7 @@ export const registerRoutes = async (
|
||||
const secretVersionTagDAL = secretVersionTagDALFactory(db);
|
||||
const secretBlindIndexDAL = secretBlindIndexDALFactory(db);
|
||||
|
||||
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
||||
const secretV2BridgeDAL = secretV2BridgeDALFactory(db);
|
||||
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
||||
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
|
||||
|
||||
@@ -391,9 +385,6 @@ export const registerRoutes = async (
|
||||
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
||||
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
||||
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
||||
const sshHostDAL = sshHostDALFactory(db);
|
||||
const sshHostLoginUserDAL = sshHostLoginUserDALFactory(db);
|
||||
const sshHostLoginUserMappingDAL = sshHostLoginUserMappingDALFactory(db);
|
||||
|
||||
const kmsDAL = kmskeyDALFactory(db);
|
||||
const internalKmsDAL = internalKmsDALFactory(db);
|
||||
@@ -805,21 +796,6 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const sshHostService = sshHostServiceFactory({
|
||||
userDAL,
|
||||
projectDAL,
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
sshCertificateDAL,
|
||||
sshCertificateBodyDAL,
|
||||
sshHostDAL,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
permissionService,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
@@ -962,7 +938,6 @@ export const registerRoutes = async (
|
||||
const projectService = projectServiceFactory({
|
||||
permissionService,
|
||||
projectDAL,
|
||||
projectSshConfigDAL,
|
||||
secretDAL,
|
||||
secretV2BridgeDAL,
|
||||
queueService,
|
||||
@@ -984,10 +959,8 @@ export const registerRoutes = async (
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
sshCertificateDAL,
|
||||
sshCertificateTemplateDAL,
|
||||
sshHostDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
keyStore,
|
||||
@@ -1391,7 +1364,8 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
projectGatewayDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
@@ -1630,7 +1604,6 @@ export const registerRoutes = async (
|
||||
certificate: certificateService,
|
||||
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||
sshCertificateTemplate: sshCertificateTemplateService,
|
||||
sshHost: sshHostService,
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
|
||||
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
|
||||
|
||||
@@ -232,7 +233,11 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||
inputIV: true,
|
||||
inputTag: true,
|
||||
algorithm: true
|
||||
});
|
||||
}).merge(
|
||||
z.object({
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
})
|
||||
);
|
||||
|
||||
export const SanitizedAuditLogStreamSchema = z.object({
|
||||
id: z.string(),
|
||||
|
@@ -12,10 +12,6 @@ import {
|
||||
AzureKeyVaultConnectionListItemSchema,
|
||||
SanitizedAzureKeyVaultConnectionSchema
|
||||
} from "@app/services/app-connection/azure-key-vault";
|
||||
import {
|
||||
CamundaConnectionListItemSchema,
|
||||
SanitizedCamundaConnectionSchema
|
||||
} from "@app/services/app-connection/camunda";
|
||||
import {
|
||||
DatabricksConnectionListItemSchema,
|
||||
SanitizedDatabricksConnectionSchema
|
||||
@@ -31,11 +27,6 @@ import {
|
||||
PostgresConnectionListItemSchema,
|
||||
SanitizedPostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
import {
|
||||
SanitizedTerraformCloudConnectionSchema,
|
||||
TerraformCloudConnectionListItemSchema
|
||||
} from "@app/services/app-connection/terraform-cloud";
|
||||
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
@@ -47,11 +38,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||
...SanitizedDatabricksConnectionSchema.options,
|
||||
...SanitizedHumanitecConnectionSchema.options,
|
||||
...SanitizedTerraformCloudConnectionSchema.options,
|
||||
...SanitizedVercelConnectionSchema.options,
|
||||
...SanitizedPostgresConnectionSchema.options,
|
||||
...SanitizedMsSqlConnectionSchema.options,
|
||||
...SanitizedCamundaConnectionSchema.options
|
||||
...SanitizedMsSqlConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -62,11 +50,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
DatabricksConnectionListItemSchema,
|
||||
HumanitecConnectionListItemSchema,
|
||||
TerraformCloudConnectionListItemSchema,
|
||||
VercelConnectionListItemSchema,
|
||||
PostgresConnectionListItemSchema,
|
||||
MsSqlConnectionListItemSchema,
|
||||
CamundaConnectionListItemSchema
|
||||
MsSqlConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
@@ -1,51 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateCamundaConnectionSchema,
|
||||
SanitizedCamundaConnectionSchema,
|
||||
UpdateCamundaConnectionSchema
|
||||
} from "@app/services/app-connection/camunda";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerCamundaConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Camunda,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedCamundaConnectionSchema,
|
||||
createSchema: CreateCamundaConnectionSchema,
|
||||
updateSchema: UpdateCamundaConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/clusters`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
clusters: z.object({ uuid: z.string(), name: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const clusters = await server.services.appConnection.camunda.listClusters(connectionId, req.permission);
|
||||
|
||||
return { clusters };
|
||||
}
|
||||
});
|
||||
};
|
@@ -3,15 +3,12 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||
|
||||
export * from "./app-connection-router";
|
||||
|
||||
@@ -24,9 +21,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
||||
[AppConnection.Vercel]: registerVercelConnectionRouter,
|
||||
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||
[AppConnection.Camunda]: registerCamundaConnectionRouter
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter
|
||||
};
|
||||
|
@@ -1,69 +0,0 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateTerraformCloudConnectionSchema,
|
||||
SanitizedTerraformCloudConnectionSchema,
|
||||
TTerraformCloudOrganization,
|
||||
UpdateTerraformCloudConnectionSchema
|
||||
} from "@app/services/app-connection/terraform-cloud";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerTerraformCloudConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.TerraformCloud,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedTerraformCloudConnectionSchema,
|
||||
createSchema: CreateTerraformCloudConnectionSchema,
|
||||
updateSchema: UpdateTerraformCloudConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/organizations`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
variableSets: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
global: z.boolean().optional()
|
||||
})
|
||||
.array(),
|
||||
workspaces: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const organizations: TTerraformCloudOrganization[] =
|
||||
await server.services.appConnection.terraformCloud.listOrganizations(connectionId, req.permission);
|
||||
|
||||
return organizations;
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,77 +0,0 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateVercelConnectionSchema,
|
||||
SanitizedVercelConnectionSchema,
|
||||
UpdateVercelConnectionSchema,
|
||||
VercelOrgWithApps
|
||||
} from "@app/services/app-connection/vercel";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerVercelConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Vercel,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedVercelConnectionSchema,
|
||||
createSchema: CreateVercelConnectionSchema,
|
||||
updateSchema: UpdateVercelConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/projects`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
apps: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
envs: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
type: z.string(),
|
||||
target: z.array(z.string()).optional(),
|
||||
description: z.string().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
updatedAt: z.number().optional()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
previewBranches: z.array(z.string()).optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const projects: VercelOrgWithApps[] = await server.services.appConnection.vercel.listProjects(
|
||||
connectionId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
};
|
@@ -4,15 +4,13 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { KMS } from "@app/lib/api-docs";
|
||||
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
||||
import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types";
|
||||
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
||||
|
||||
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
||||
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||
@@ -47,46 +45,16 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
description: "Create KMS key",
|
||||
body: z
|
||||
.object({
|
||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||
keyUsage: z
|
||||
.nativeEnum(KmsKeyUsage)
|
||||
.optional()
|
||||
.default(KmsKeyUsage.ENCRYPT_DECRYPT)
|
||||
.describe(KMS.CREATE_KEY.type),
|
||||
encryptionAlgorithm: z
|
||||
.enum(AllowedEncryptionKeyAlgorithms)
|
||||
.optional()
|
||||
.default(SymmetricKeyAlgorithm.AES_GCM_256)
|
||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT &&
|
||||
!Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values(
|
||||
SymmetricKeyAlgorithm
|
||||
).join(", ")}`
|
||||
});
|
||||
}
|
||||
if (
|
||||
data.keyUsage === KmsKeyUsage.SIGN_VERIFY &&
|
||||
!Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values(
|
||||
AsymmetricKeyAlgorithm
|
||||
).join(", ")}`
|
||||
});
|
||||
}
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||
encryptionAlgorithm: z
|
||||
.nativeEnum(SymmetricEncryption)
|
||||
.optional()
|
||||
.default(SymmetricEncryption.AES_GCM_256)
|
||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
key: CmekSchema
|
||||
@@ -96,19 +64,12 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
body: { projectId, name, description, encryptionAlgorithm, keyUsage },
|
||||
body: { projectId, name, description, encryptionAlgorithm },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const cmek = await server.services.cmek.createCmek(
|
||||
{
|
||||
orgId: permission.orgId,
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm,
|
||||
keyUsage
|
||||
},
|
||||
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
|
||||
permission
|
||||
);
|
||||
|
||||
@@ -121,7 +82,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
keyId: cmek.id,
|
||||
name,
|
||||
description,
|
||||
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
|
||||
encryptionAlgorithm
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -165,7 +126,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: cmek.projectId!,
|
||||
orgId: permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CMEK,
|
||||
metadata: {
|
||||
@@ -208,7 +169,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: cmek.projectId!,
|
||||
orgId: permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_CMEK,
|
||||
metadata: {
|
||||
@@ -321,7 +282,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get KMS key by name",
|
||||
description: "Get KMS key by Name",
|
||||
params: z.object({
|
||||
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
||||
}),
|
||||
@@ -388,11 +349,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { ciphertext, projectId } = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
||||
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
orgId: permission.orgId,
|
||||
event: {
|
||||
type: EventType.CMEK_ENCRYPT,
|
||||
metadata: {
|
||||
@@ -405,198 +366,6 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/keys/:keyId/public-key",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description:
|
||||
"Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
publicKey: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { publicKey, projectId } = await server.services.cmek.getPublicKey({ keyId }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_GET_PUBLIC_KEY,
|
||||
metadata: {
|
||||
keyId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { publicKey };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/keys/:keyId/signing-algorithms",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List all available signing algorithms for a KMS key",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm))
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { keyId } = req.params;
|
||||
|
||||
const { signingAlgorithms, projectId } = await server.services.cmek.listSigningAlgorithms(
|
||||
{ keyId },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS,
|
||||
metadata: {
|
||||
keyId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { signingAlgorithms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/sign",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Sign data with a KMS key.",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.SIGN.keyId)
|
||||
}),
|
||||
body: z.object({
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||
isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest),
|
||||
data: base64Schema.describe(KMS.SIGN.data)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signature: z.string(),
|
||||
keyId: z.string().uuid(),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId: inputKeyId },
|
||||
body: { data, signingAlgorithm, isDigest },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { projectId, ...result } = await server.services.cmek.cmekSign(
|
||||
{ keyId: inputKeyId, data, signingAlgorithm, isDigest },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_SIGN,
|
||||
metadata: {
|
||||
keyId: inputKeyId,
|
||||
signingAlgorithm,
|
||||
signature: result.signature
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/verify",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Verify data signatures with a KMS key.",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.VERIFY.keyId)
|
||||
}),
|
||||
body: z.object({
|
||||
isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest),
|
||||
data: base64Schema.describe(KMS.VERIFY.data),
|
||||
signature: base64Schema.describe(KMS.VERIFY.signature),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signatureValid: z.boolean(),
|
||||
keyId: z.string().uuid(),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId },
|
||||
body: { data, signature, signingAlgorithm, isDigest },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { projectId, ...result } = await server.services.cmek.cmekVerify(
|
||||
{ keyId, data, signature, signingAlgorithm, isDigest },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_VERIFY,
|
||||
metadata: {
|
||||
keyId,
|
||||
signatureValid: result.signatureValid,
|
||||
signingAlgorithm,
|
||||
signature
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/decrypt",
|
||||
@@ -625,11 +394,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { plaintext, projectId } = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
||||
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
orgId: permission.orgId,
|
||||
event: {
|
||||
type: EventType.CMEK_DECRYPT,
|
||||
metadata: {
|
||||
|
@@ -1,13 +1,9 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
||||
import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
import { DASHBOARD } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@@ -289,24 +285,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalCount: totalFolderCount ?? 0
|
||||
};
|
||||
|
||||
const { permission } = await server.services.permission.getProjectPermission({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
const allowedDynamicSecretEnvironments = // filter envs user has access to
|
||||
environments.filter((environment) =>
|
||||
permission.can(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
|
||||
)
|
||||
);
|
||||
|
||||
if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) {
|
||||
if (includeDynamicSecrets) {
|
||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||
actor: req.permission.type,
|
||||
@@ -315,7 +294,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
search,
|
||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
||||
environmentSlugs: environments,
|
||||
path: secretPath,
|
||||
isInternal: true
|
||||
});
|
||||
@@ -330,7 +309,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
||||
environmentSlugs: environments,
|
||||
path: secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
|
@@ -1,13 +0,0 @@
|
||||
import { CamundaSyncSchema, CreateCamundaSyncSchema, UpdateCamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerCamundaSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Camunda,
|
||||
server,
|
||||
responseSchema: CamundaSyncSchema,
|
||||
createSchema: CreateCamundaSyncSchema,
|
||||
updateSchema: UpdateCamundaSyncSchema
|
||||
});
|
@@ -4,13 +4,10 @@ import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-
|
||||
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
|
||||
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
|
||||
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
||||
|
||||
export * from "./secret-sync-router";
|
||||
|
||||
@@ -22,8 +19,5 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
|
||||
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
||||
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||
[SecretSync.Vercel]: registerVercelSyncRouter
|
||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter
|
||||
};
|
||||
|
@@ -18,13 +18,10 @@ import {
|
||||
AzureAppConfigurationSyncSchema
|
||||
} from "@app/services/secret-sync/azure-app-configuration";
|
||||
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
|
||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncSchema,
|
||||
@@ -34,10 +31,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AzureKeyVaultSyncSchema,
|
||||
AzureAppConfigurationSyncSchema,
|
||||
DatabricksSyncSchema,
|
||||
HumanitecSyncSchema,
|
||||
TerraformCloudSyncSchema,
|
||||
CamundaSyncSchema,
|
||||
VercelSyncSchema
|
||||
HumanitecSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -48,10 +42,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
AzureKeyVaultSyncListItemSchema,
|
||||
AzureAppConfigurationSyncListItemSchema,
|
||||
DatabricksSyncListItemSchema,
|
||||
HumanitecSyncListItemSchema,
|
||||
TerraformCloudSyncListItemSchema,
|
||||
CamundaSyncListItemSchema,
|
||||
VercelSyncListItemSchema
|
||||
HumanitecSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
@@ -1,17 +0,0 @@
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
CreateTerraformCloudSyncSchema,
|
||||
TerraformCloudSyncSchema,
|
||||
UpdateTerraformCloudSyncSchema
|
||||
} from "@app/services/secret-sync/terraform-cloud";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerTerraformCloudSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.TerraformCloud,
|
||||
server,
|
||||
responseSchema: TerraformCloudSyncSchema,
|
||||
createSchema: CreateTerraformCloudSyncSchema,
|
||||
updateSchema: UpdateTerraformCloudSyncSchema
|
||||
});
|
@@ -1,13 +0,0 @@
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { CreateVercelSyncSchema, UpdateVercelSyncSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerVercelSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Vercel,
|
||||
server,
|
||||
responseSchema: VercelSyncSchema,
|
||||
createSchema: CreateVercelSyncSchema,
|
||||
updateSchema: UpdateVercelSyncSchema
|
||||
});
|
@@ -108,7 +108,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
firstName: profile.displayName,
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
callbackPort
|
||||
@@ -145,7 +145,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const email = profile.emails[0].value;
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
firstName: profile.displayName,
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITLAB,
|
||||
callbackPort
|
||||
|
@@ -351,56 +351,4 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
return { identityMembership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/identity-memberships/:identityMembershipId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
identityMembershipId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityMembership: z.object({
|
||||
id: z.string(),
|
||||
identityId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
role: z.string(),
|
||||
customRoleId: z.string().optional().nullable(),
|
||||
customRoleName: z.string().optional().nullable(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryMode: z.string().optional().nullable(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityMembership = await server.services.identityProject.getProjectIdentityByMembershipId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityMembershipId: req.params.identityMembershipId
|
||||
});
|
||||
return { identityMembership };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -13,7 +13,6 @@ import { InfisicalProjectTemplate } from "@app/ee/services/project-template/proj
|
||||
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||
import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema";
|
||||
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
@@ -601,38 +600,4 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
return { cas };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/ssh-hosts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
hosts: z.array(
|
||||
sanitizedSshHost.extend({
|
||||
loginMappings: z.array(loginMappingSchema)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const hosts = await server.services.project.listProjectSshHosts({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
|
||||
return { hosts };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -6,11 +6,8 @@ export enum AppConnection {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Vercel = "vercel",
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda"
|
||||
MsSql = "mssql"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@@ -27,7 +27,6 @@ import {
|
||||
getAzureKeyVaultConnectionListItem,
|
||||
validateAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault";
|
||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
@@ -42,13 +41,6 @@ import {
|
||||
} from "./humanitec";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import {
|
||||
getTerraformCloudConnectionListItem,
|
||||
TerraformCloudConnectionMethod,
|
||||
validateTerraformCloudConnectionCredentials
|
||||
} from "./terraform-cloud";
|
||||
import { VercelConnectionMethod } from "./vercel";
|
||||
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [
|
||||
@@ -59,11 +51,8 @@ export const listAppConnectionOptions = () => {
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getTerraformCloudConnectionListItem(),
|
||||
getVercelConnectionListItem(),
|
||||
getPostgresConnectionListItem(),
|
||||
getMsSqlConnectionListItem(),
|
||||
getCamundaConnectionListItem()
|
||||
getMsSqlConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -119,10 +108,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnect
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
export const validateAppConnectionCredentials = async (
|
||||
@@ -145,11 +131,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
return "Service Account Impersonation";
|
||||
case DatabricksConnectionMethod.ServicePrincipal:
|
||||
return "Service Principal";
|
||||
case CamundaConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
case HumanitecConnectionMethod.ApiToken:
|
||||
case TerraformCloudConnectionMethod.ApiToken:
|
||||
case VercelConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
@@ -193,8 +175,5 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform
|
||||
};
|
||||
|
@@ -8,9 +8,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||
[AppConnection.Vercel]: "Vercel",
|
||||
[AppConnection.Postgres]: "PostgreSQL",
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda"
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server"
|
||||
};
|
||||
|
@@ -31,8 +31,6 @@ import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
@@ -43,10 +41,6 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@@ -64,11 +58,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -439,9 +430,6 @@ export const appConnectionServiceFactory = ({
|
||||
gcp: gcpConnectionService(connectAppConnectionById),
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
aws: awsConnectionService(connectAppConnectionById),
|
||||
humanitec: humanitecConnectionService(connectAppConnectionById),
|
||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById)
|
||||
humanitec: humanitecConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user