mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
105 Commits
ssh-cli
...
misc/add-c
Author | SHA1 | Date | |
---|---|---|---|
2357f3bc1f | |||
cde813aafb | |||
bbc8091d44 | |||
ce5e591457 | |||
5ae74f9761 | |||
eef331bbd1 | |||
d5c2e9236a | |||
13eef7e524 | |||
3fa84c578c | |||
c22ed04733 | |||
64fac1979f | |||
2d60f389c2 | |||
7798e5a2ad | |||
ed78227725 | |||
89848a2f5c | |||
1936f7cc4f | |||
1adeb5a70d | |||
8122433f5c | |||
a0411e3ba8 | |||
62968c5e43 | |||
f3cf1a3f50 | |||
fed99a14a8 | |||
d4cfee99a6 | |||
e70ca57510 | |||
06f321e4bf | |||
3c3fcd0db8 | |||
21eb2bed7e | |||
381960b0bd | |||
7eb05afe2a | |||
0b54948b15 | |||
39e598e408 | |||
b735618601 | |||
3a5e862def | |||
d1c4a9c75a | |||
5532844ee7 | |||
dd5aab973f | |||
ced12baf5d | |||
7db1e62654 | |||
0ab3ae442e | |||
ed9472efc8 | |||
e094844601 | |||
e761b49964 | |||
6a8be75b79 | |||
a92e61575d | |||
761007208d | |||
cc3e0d1922 | |||
215761ca6b | |||
0977ff1e36 | |||
c6081900a4 | |||
86800c0cdb | |||
1fa99e5585 | |||
7947e73569 | |||
8f5bb44ff4 | |||
3f70f08e8c | |||
078eaff164 | |||
221aa99374 | |||
6a681dcf6a | |||
b99b98b6a4 | |||
d7271b9631 | |||
379e526200 | |||
1f151a9b05 | |||
6b2eb9c6c9 | |||
52ce90846a | |||
68a3291235 | |||
471f47d260 | |||
ccb757ec3e | |||
b669b0a9f8 | |||
9e768640cd | |||
35f7420447 | |||
c6a0e36318 | |||
181ba75f2a | |||
c00f6601bd | |||
111605a945 | |||
2ac110f00e | |||
0366506213 | |||
e3d29b637d | |||
9cd0dc8970 | |||
f8f5000bad | |||
40919ccf59 | |||
44303aca6a | |||
4bd50c3548 | |||
fb253d00eb | |||
1cbf030e6c | |||
f5920f416a | |||
3b2154bab4 | |||
7c8f2e5548 | |||
c5816014a6 | |||
a730b16318 | |||
cc3d132f5d | |||
48174e2500 | |||
7cf297344b | |||
42249726d4 | |||
ec1ce3dc06 | |||
82a4b89bb5 | |||
ff3d8c896b | |||
6e720c2f64 | |||
5b618b07fa | |||
a5a1f57284 | |||
8327f6154e | |||
20a9fc113c | |||
8edfa9ad0b | |||
00ce755996 | |||
3b2173a098 | |||
07d9398aad | |||
4fc8c509ac |
17
.env.example
17
.env.example
@ -88,3 +88,20 @@ PLAIN_WISH_LABEL_IDS=
|
|||||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||||
|
|
||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true
|
||||||
|
|
||||||
|
# App Connections
|
||||||
|
|
||||||
|
# aws assume-role
|
||||||
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID=
|
||||||
|
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
|
# github oauth
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID=
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET=
|
||||||
|
|
||||||
|
#github app
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_ID=
|
@ -137,6 +137,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
freetds-dev \
|
freetds-dev \
|
||||||
freetds-bin \
|
freetds-bin \
|
||||||
tdsodbc \
|
tdsodbc \
|
||||||
|
openssh \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Configure ODBC in production
|
# Configure ODBC in production
|
||||||
|
@ -139,7 +139,8 @@ RUN apk --update add \
|
|||||||
freetds-dev \
|
freetds-dev \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
git
|
git \
|
||||||
|
openssh
|
||||||
|
|
||||||
# Configure ODBC in production
|
# Configure ODBC in production
|
||||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||||
|
@ -7,7 +7,8 @@ WORKDIR /app
|
|||||||
RUN apk --update add \
|
RUN apk --update add \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++
|
g++ \
|
||||||
|
openssh
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
|
@ -17,7 +17,8 @@ RUN apk --update add \
|
|||||||
openssl-dev \
|
openssl-dev \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++
|
g++ \
|
||||||
|
openssh
|
||||||
|
|
||||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
|
@ -22,8 +22,10 @@ export const mockQueue = (): TQueueServiceFactory => {
|
|||||||
listen: (name, event) => {
|
listen: (name, event) => {
|
||||||
events[name] = event;
|
events[name] = event;
|
||||||
},
|
},
|
||||||
|
getRepeatableJobs: async () => [],
|
||||||
clearQueue: async () => {},
|
clearQueue: async () => {},
|
||||||
stopJobById: async () => {},
|
stopJobById: async () => {},
|
||||||
stopRepeatableJobByJobId: async () => true
|
stopRepeatableJobByJobId: async () => true,
|
||||||
|
stopRepeatableJobByKey: async () => true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@ -31,9 +31,12 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
|
|||||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
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 { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
@ -177,6 +180,8 @@ declare module "fastify" {
|
|||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
certificate: TCertificateServiceFactory;
|
certificate: TCertificateServiceFactory;
|
||||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
|
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||||
|
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
@ -204,6 +209,7 @@ declare module "fastify" {
|
|||||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
projectTemplate: TProjectTemplateServiceFactory;
|
projectTemplate: TProjectTemplateServiceFactory;
|
||||||
totp: TTotpServiceFactory;
|
totp: TTotpServiceFactory;
|
||||||
|
appConnection: TAppConnectionServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
46
backend/src/@types/knex.d.ts
vendored
46
backend/src/@types/knex.d.ts
vendored
@ -317,6 +317,21 @@ import {
|
|||||||
TSlackIntegrations,
|
TSlackIntegrations,
|
||||||
TSlackIntegrationsInsert,
|
TSlackIntegrationsInsert,
|
||||||
TSlackIntegrationsUpdate,
|
TSlackIntegrationsUpdate,
|
||||||
|
TSshCertificateAuthorities,
|
||||||
|
TSshCertificateAuthoritiesInsert,
|
||||||
|
TSshCertificateAuthoritiesUpdate,
|
||||||
|
TSshCertificateAuthoritySecrets,
|
||||||
|
TSshCertificateAuthoritySecretsInsert,
|
||||||
|
TSshCertificateAuthoritySecretsUpdate,
|
||||||
|
TSshCertificateBodies,
|
||||||
|
TSshCertificateBodiesInsert,
|
||||||
|
TSshCertificateBodiesUpdate,
|
||||||
|
TSshCertificates,
|
||||||
|
TSshCertificatesInsert,
|
||||||
|
TSshCertificatesUpdate,
|
||||||
|
TSshCertificateTemplates,
|
||||||
|
TSshCertificateTemplatesInsert,
|
||||||
|
TSshCertificateTemplatesUpdate,
|
||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
@ -348,6 +363,7 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate } from "@app/db/schemas/app-connections";
|
||||||
import {
|
import {
|
||||||
TExternalGroupOrgRoleMappings,
|
TExternalGroupOrgRoleMappings,
|
||||||
TExternalGroupOrgRoleMappingsInsert,
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
@ -378,6 +394,31 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateAuthorities,
|
||||||
|
TSshCertificateAuthoritiesInsert,
|
||||||
|
TSshCertificateAuthoritiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateAuthoritySecret]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateAuthoritySecrets,
|
||||||
|
TSshCertificateAuthoritySecretsInsert,
|
||||||
|
TSshCertificateAuthoritySecretsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateTemplates,
|
||||||
|
TSshCertificateTemplatesInsert,
|
||||||
|
TSshCertificateTemplatesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificate]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificates,
|
||||||
|
TSshCertificatesInsert,
|
||||||
|
TSshCertificatesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshCertificateBody]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshCertificateBodies,
|
||||||
|
TSshCertificateBodiesInsert,
|
||||||
|
TSshCertificateBodiesUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateAuthorities,
|
TCertificateAuthorities,
|
||||||
TCertificateAuthoritiesInsert,
|
TCertificateAuthoritiesInsert,
|
||||||
@ -846,5 +887,10 @@ declare module "knex/types/tables" {
|
|||||||
TProjectSplitBackfillIdsInsert,
|
TProjectSplitBackfillIdsInsert,
|
||||||
TProjectSplitBackfillIdsUpdate
|
TProjectSplitBackfillIdsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
|
||||||
|
TAppConnections,
|
||||||
|
TAppConnectionsInsert,
|
||||||
|
TAppConnectionsUpdate
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
99
backend/src/db/migrations/20241216013357_ssh-mgmt.ts
Normal file
99
backend/src/db/migrations/20241216013357_ssh-mgmt.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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.SshCertificateAuthority))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateAuthority, (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("status").notNullable(); // active / disabled
|
||||||
|
t.string("friendlyName").notNullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateAuthoritySecret))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateAuthoritySecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable().unique();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateTemplate))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateTemplate, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("status").notNullable(); // active / disabled
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.string("ttl").notNullable();
|
||||||
|
t.string("maxTTL").notNullable();
|
||||||
|
t.specificType("allowedUsers", "text[]").notNullable();
|
||||||
|
t.specificType("allowedHosts", "text[]").notNullable();
|
||||||
|
t.boolean("allowUserCertificates").notNullable();
|
||||||
|
t.boolean("allowHostCertificates").notNullable();
|
||||||
|
t.boolean("allowCustomKeyIds").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificate))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificate, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCaId").notNullable();
|
||||||
|
t.foreign("sshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||||
|
t.uuid("sshCertificateTemplateId");
|
||||||
|
t.foreign("sshCertificateTemplateId")
|
||||||
|
.references("id")
|
||||||
|
.inTable(TableName.SshCertificateTemplate)
|
||||||
|
.onDelete("SET NULL");
|
||||||
|
t.string("serialNumber").notNullable().unique();
|
||||||
|
t.string("certType").notNullable(); // user or host
|
||||||
|
t.specificType("principals", "text[]").notNullable();
|
||||||
|
t.string("keyId").notNullable();
|
||||||
|
t.datetime("notBefore").notNullable();
|
||||||
|
t.datetime("notAfter").notNullable();
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshCertificateBody))) {
|
||||||
|
await knex.schema.createTable(TableName.SshCertificateBody, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshCertId").notNullable().unique();
|
||||||
|
t.foreign("sshCertId").references("id").inTable(TableName.SshCertificate).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCertificate").notNullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshCertificateBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateBody);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateBody);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificate);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificate);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateTemplate);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateTemplate);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthoritySecret);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthoritySecret);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshCertificateAuthority);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshCertificateAuthority);
|
||||||
|
}
|
28
backend/src/db/migrations/20241218181018_app-connection.ts
Normal file
28
backend/src/db/migrations/20241218181018_app-connection.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.AppConnection))) {
|
||||||
|
await knex.schema.createTable(TableName.AppConnection, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name", 32).notNullable();
|
||||||
|
t.string("description");
|
||||||
|
t.string("app").notNullable();
|
||||||
|
t.string("method").notNullable();
|
||||||
|
t.binary("encryptedCredentials").notNullable();
|
||||||
|
t.integer("version").defaultTo(1).notNullable();
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.AppConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.AppConnection);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.AppConnection);
|
||||||
|
}
|
27
backend/src/db/schemas/app-connections.ts
Normal file
27
backend/src/db/schemas/app-connections.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const AppConnectionsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
app: z.string(),
|
||||||
|
method: z.string(),
|
||||||
|
encryptedCredentials: zodBuffer,
|
||||||
|
version: z.number().default(1),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||||
|
export type TAppConnectionsInsert = Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TAppConnectionsUpdate = Partial<Omit<z.input<typeof AppConnectionsSchema>, TImmutableDBKeys>>;
|
@ -107,6 +107,11 @@ export * from "./secrets";
|
|||||||
export * from "./secrets-v2";
|
export * from "./secrets-v2";
|
||||||
export * from "./service-tokens";
|
export * from "./service-tokens";
|
||||||
export * from "./slack-integrations";
|
export * from "./slack-integrations";
|
||||||
|
export * from "./ssh-certificate-authorities";
|
||||||
|
export * from "./ssh-certificate-authority-secrets";
|
||||||
|
export * from "./ssh-certificate-bodies";
|
||||||
|
export * from "./ssh-certificate-templates";
|
||||||
|
export * from "./ssh-certificates";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
export * from "./totp-configs";
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
|
@ -2,6 +2,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
SshCertificateAuthority = "ssh_certificate_authorities",
|
||||||
|
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
||||||
|
SshCertificateTemplate = "ssh_certificate_templates",
|
||||||
|
SshCertificate = "ssh_certificates",
|
||||||
|
SshCertificateBody = "ssh_certificate_bodies",
|
||||||
CertificateAuthority = "certificate_authorities",
|
CertificateAuthority = "certificate_authorities",
|
||||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||||
CertificateAuthorityCert = "certificate_authority_certs",
|
CertificateAuthorityCert = "certificate_authority_certs",
|
||||||
@ -124,7 +129,8 @@ export enum TableName {
|
|||||||
KmsKeyVersion = "kms_key_versions",
|
KmsKeyVersion = "kms_key_versions",
|
||||||
WorkflowIntegrations = "workflow_integrations",
|
WorkflowIntegrations = "workflow_integrations",
|
||||||
SlackIntegrations = "slack_integrations",
|
SlackIntegrations = "slack_integrations",
|
||||||
ProjectSlackConfigs = "project_slack_configs"
|
ProjectSlackConfigs = "project_slack_configs",
|
||||||
|
AppConnection = "app_connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
@ -205,5 +211,6 @@ export enum IdentityAuthMethod {
|
|||||||
export enum ProjectType {
|
export enum ProjectType {
|
||||||
SecretManager = "secret-manager",
|
SecretManager = "secret-manager",
|
||||||
CertificateManager = "cert-manager",
|
CertificateManager = "cert-manager",
|
||||||
KMS = "kms"
|
KMS = "kms",
|
||||||
|
SSH = "ssh"
|
||||||
}
|
}
|
||||||
|
24
backend/src/db/schemas/ssh-certificate-authorities.ts
Normal file
24
backend/src/db/schemas/ssh-certificate-authorities.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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 SshCertificateAuthoritiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
friendlyName: z.string(),
|
||||||
|
keyAlgorithm: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
||||||
|
export type TSshCertificateAuthoritiesInsert = Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateAuthoritiesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
27
backend/src/db/schemas/ssh-certificate-authority-secrets.ts
Normal file
27
backend/src/db/schemas/ssh-certificate-authority-secrets.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshCertificateAuthoritySecretsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCaId: z.string().uuid(),
|
||||||
|
encryptedPrivateKey: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateAuthoritySecrets = z.infer<typeof SshCertificateAuthoritySecretsSchema>;
|
||||||
|
export type TSshCertificateAuthoritySecretsInsert = Omit<
|
||||||
|
z.input<typeof SshCertificateAuthoritySecretsSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TSshCertificateAuthoritySecretsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateAuthoritySecretsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
22
backend/src/db/schemas/ssh-certificate-bodies.ts
Normal file
22
backend/src/db/schemas/ssh-certificate-bodies.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshCertificateBodiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCertId: z.string().uuid(),
|
||||||
|
encryptedCertificate: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateBodies = z.infer<typeof SshCertificateBodiesSchema>;
|
||||||
|
export type TSshCertificateBodiesInsert = Omit<z.input<typeof SshCertificateBodiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateBodiesUpdate = Partial<Omit<z.input<typeof SshCertificateBodiesSchema>, TImmutableDBKeys>>;
|
30
backend/src/db/schemas/ssh-certificate-templates.ts
Normal file
30
backend/src/db/schemas/ssh-certificate-templates.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// 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 SshCertificateTemplatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshCaId: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
ttl: z.string(),
|
||||||
|
maxTTL: z.string(),
|
||||||
|
allowedUsers: z.string().array(),
|
||||||
|
allowedHosts: z.string().array(),
|
||||||
|
allowUserCertificates: z.boolean(),
|
||||||
|
allowHostCertificates: z.boolean(),
|
||||||
|
allowCustomKeyIds: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificateTemplates = z.infer<typeof SshCertificateTemplatesSchema>;
|
||||||
|
export type TSshCertificateTemplatesInsert = Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificateTemplatesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshCertificateTemplatesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
26
backend/src/db/schemas/ssh-certificates.ts
Normal file
26
backend/src/db/schemas/ssh-certificates.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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 SshCertificatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
||||||
|
export type TSshCertificatesInsert = Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshCertificatesUpdate = Partial<Omit<z.input<typeof SshCertificatesSchema>, TImmutableDBKeys>>;
|
@ -25,6 +25,9 @@ import { registerSecretRotationRouter } from "./secret-rotation-router";
|
|||||||
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
||||||
import { registerSecretVersionRouter } from "./secret-version-router";
|
import { registerSecretVersionRouter } from "./secret-version-router";
|
||||||
import { registerSnapshotRouter } from "./snapshot-router";
|
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 { registerTrustedIpRouter } from "./trusted-ip-router";
|
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||||
|
|
||||||
@ -68,6 +71,15 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/pki" }
|
{ prefix: "/pki" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (sshRouter) => {
|
||||||
|
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
||||||
|
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
||||||
|
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
|
},
|
||||||
|
{ prefix: "/ssh" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (ssoRouter) => {
|
async (ssoRouter) => {
|
||||||
await ssoRouter.register(registerSamlRouter);
|
await ssoRouter.register(registerSamlRouter);
|
||||||
|
@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
),
|
),
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array()
|
permissions: z.any().array()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -95,7 +95,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array().optional()
|
permissions: z.any().array().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@ -39,7 +39,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -95,7 +95,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||||
.optional(),
|
.optional(),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
279
backend/src/ee/routes/v1/ssh-certificate-authority-router.ts
Normal file
279
backend/src/ee/routes/v1/ssh-certificate-authority-router.ts
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
|
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({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
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(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa.extend({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.createSshCa({
|
||||||
|
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: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa.extend({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.getSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId/public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get public key of SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_PUBLIC_KEY.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKey = await server.services.sshCertificateAuthority.getSshCaPublicKey({
|
||||||
|
caId: req.params.sshCaId
|
||||||
|
});
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.sshCaId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
friendlyName: z.string().optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.friendlyName),
|
||||||
|
status: z.nativeEnum(SshCaStatus).optional().describe(SSH_CERTIFICATE_AUTHORITIES.UPDATE.status)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa.extend({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.updateSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
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: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName,
|
||||||
|
status: ca.status as SshCaStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sshCaId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Delete SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.DELETE.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: sanitizedSshCa
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.sshCertificateAuthority.deleteSshCaById({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_CA,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshCaId/certificate-templates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get list of certificate templates for the SSH CA",
|
||||||
|
params: z.object({
|
||||||
|
sshCaId: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.GET_CERTIFICATE_TEMPLATES.sshCaId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificateTemplates: sanitizedSshCertificateTemplate.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplates, ca } = await server.services.sshCertificateAuthority.getSshCaCertificateTemplates({
|
||||||
|
caId: req.params.sshCaId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES,
|
||||||
|
metadata: {
|
||||||
|
sshCaId: ca.id,
|
||||||
|
friendlyName: ca.friendlyName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplates
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
164
backend/src/ee/routes/v1/ssh-certificate-router.ts
Normal file
164
backend/src/ee/routes/v1/ssh-certificate-router.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
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 { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
import { 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 registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/sign",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Sign SSH public key",
|
||||||
|
body: z.object({
|
||||||
|
certificateTemplateId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certificateTemplateId),
|
||||||
|
publicKey: z.string().trim().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.publicKey),
|
||||||
|
certType: z
|
||||||
|
.nativeEnum(SshCertType)
|
||||||
|
.default(SshCertType.USER)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.certType),
|
||||||
|
principals: z
|
||||||
|
.array(z.string().transform((val) => val.trim()))
|
||||||
|
.nonempty("Principals array must not be empty")
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.principals),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.ttl),
|
||||||
|
keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.serialNumber),
|
||||||
|
signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.SIGN_SSH_KEY.signedKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { serialNumber, signedPublicKey, certificateTemplate, ttl, keyId } =
|
||||||
|
await server.services.sshCertificateAuthority.signSshKey({
|
||||||
|
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,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SIGN_SSH_KEY,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
certType: req.body.certType,
|
||||||
|
principals: req.body.principals,
|
||||||
|
ttl: String(ttl),
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/issue",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue SSH credentials (certificate + key)",
|
||||||
|
body: z.object({
|
||||||
|
certificateTemplateId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
|
||||||
|
certType: z
|
||||||
|
.nativeEnum(SshCertType)
|
||||||
|
.default(SshCertType.USER)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certType),
|
||||||
|
principals: z
|
||||||
|
.array(z.string().transform((val) => val.trim()))
|
||||||
|
.nonempty("Principals array must not be empty")
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.principals),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.ttl),
|
||||||
|
keyId: z.string().trim().max(50).optional().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serialNumber: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.serialNumber),
|
||||||
|
signedKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.signedKey),
|
||||||
|
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(CertKeyAlgorithm)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { serialNumber, signedPublicKey, privateKey, publicKey, certificateTemplate, ttl, keyId } =
|
||||||
|
await server.services.sshCertificateAuthority.issueSshCreds({
|
||||||
|
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,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ISSUE_SSH_CREDS,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm,
|
||||||
|
certType: req.body.certType,
|
||||||
|
principals: req.body.principals,
|
||||||
|
ttl: String(ttl),
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
258
backend/src/ee/routes/v1/ssh-certificate-template-router.ts
Normal file
258
backend/src/ee/routes/v1/ssh-certificate-template-router.ts
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
|
import {
|
||||||
|
isValidHostPattern,
|
||||||
|
isValidUserPattern
|
||||||
|
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
|
import { SSH_CERTIFICATE_TEMPLATES } 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";
|
||||||
|
|
||||||
|
export const registerSshCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const certificateTemplate = await server.services.sshCertificateTemplate.getSshCertTemplate({
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: certificateTemplate.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
sshCaId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.sshCaId),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(36)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Name must be a valid slug"
|
||||||
|
})
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.name),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.default("1h")
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
|
||||||
|
.default("30d")
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.maxTTL),
|
||||||
|
allowedUsers: z
|
||||||
|
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedUsers),
|
||||||
|
allowedHosts: z
|
||||||
|
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowedHosts),
|
||||||
|
allowUserCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowUserCertificates),
|
||||||
|
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 TTL",
|
||||||
|
path: ["maxTTL"]
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplate, ca } = await server.services.sshCertificateTemplate.createSshCertTemplate({
|
||||||
|
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: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
sshCaId: ca.id,
|
||||||
|
name: certificateTemplate.name,
|
||||||
|
ttl: certificateTemplate.ttl,
|
||||||
|
maxTTL: certificateTemplate.maxTTL,
|
||||||
|
allowedUsers: certificateTemplate.allowedUsers,
|
||||||
|
allowedHosts: certificateTemplate.allowedHosts,
|
||||||
|
allowUserCertificates: certificateTemplate.allowUserCertificates,
|
||||||
|
allowHostCertificates: certificateTemplate.allowHostCertificates,
|
||||||
|
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
status: z.nativeEnum(SshCertTemplateStatus).optional(),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(36)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Slug must be a valid slug"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.name),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.ttl),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "Max TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.maxTTL),
|
||||||
|
allowedUsers: z
|
||||||
|
.array(z.string().refine(isValidUserPattern, "Invalid user pattern"))
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedUsers),
|
||||||
|
allowedHosts: z
|
||||||
|
.array(z.string().refine(isValidHostPattern, "Invalid host pattern"))
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowedHosts),
|
||||||
|
allowUserCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowUserCertificates),
|
||||||
|
allowHostCertificates: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowHostCertificates),
|
||||||
|
allowCustomKeyIds: z.boolean().optional().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.allowCustomKeyIds)
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplate, projectId } = await server.services.sshCertificateTemplate.updateSshCertTemplate({
|
||||||
|
...req.body,
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
status: certificateTemplate.status as SshCertTemplateStatus,
|
||||||
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
sshCaId: certificateTemplate.sshCaId,
|
||||||
|
name: certificateTemplate.name,
|
||||||
|
ttl: certificateTemplate.ttl,
|
||||||
|
maxTTL: certificateTemplate.maxTTL,
|
||||||
|
allowedUsers: certificateTemplate.allowedUsers,
|
||||||
|
allowedHosts: certificateTemplate.allowedHosts,
|
||||||
|
allowUserCertificates: certificateTemplate.allowUserCertificates,
|
||||||
|
allowHostCertificates: certificateTemplate.allowHostCertificates,
|
||||||
|
allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:certificateTemplateId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
certificateTemplateId: z.string().describe(SSH_CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshCertificateTemplate
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const certificateTemplate = await server.services.sshCertificateTemplate.deleteSshCertTemplate({
|
||||||
|
id: req.params.certificateTemplateId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: certificateTemplate.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE,
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: certificateTemplate.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -36,7 +36,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -91,7 +91,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_ROLE.UPDATE.slug),
|
.describe(PROJECT_ROLE.UPDATE.slug),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@ -2,9 +2,14 @@ import {
|
|||||||
TCreateProjectTemplateDTO,
|
TCreateProjectTemplateDTO,
|
||||||
TUpdateProjectTemplateDTO
|
TUpdateProjectTemplateDTO
|
||||||
} from "@app/ee/services/project-template/project-template-types";
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
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";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||||
@ -143,6 +148,17 @@ export enum EventType {
|
|||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
||||||
|
SIGN_SSH_KEY = "sign-ssh-key",
|
||||||
|
ISSUE_SSH_CREDS = "issue-ssh-creds",
|
||||||
|
CREATE_SSH_CA = "create-ssh-certificate-authority",
|
||||||
|
GET_SSH_CA = "get-ssh-certificate-authority",
|
||||||
|
UPDATE_SSH_CA = "update-ssh-certificate-authority",
|
||||||
|
DELETE_SSH_CA = "delete-ssh-certificate-authority",
|
||||||
|
GET_SSH_CA_CERTIFICATE_TEMPLATES = "get-ssh-certificate-authority-certificate-templates",
|
||||||
|
CREATE_SSH_CERTIFICATE_TEMPLATE = "create-ssh-certificate-template",
|
||||||
|
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_CA = "create-certificate-authority",
|
CREATE_CA = "create-certificate-authority",
|
||||||
GET_CA = "get-certificate-authority",
|
GET_CA = "get-certificate-authority",
|
||||||
UPDATE_CA = "update-certificate-authority",
|
UPDATE_CA = "update-certificate-authority",
|
||||||
@ -208,7 +224,12 @@ export enum EventType {
|
|||||||
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
||||||
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
||||||
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
||||||
APPLY_PROJECT_TEMPLATE = "apply-project-template"
|
APPLY_PROJECT_TEMPLATE = "apply-project-template",
|
||||||
|
GET_APP_CONNECTIONS = "get-app-connections",
|
||||||
|
GET_APP_CONNECTION = "get-app-connection",
|
||||||
|
CREATE_APP_CONNECTION = "create-app-connection",
|
||||||
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
|
DELETE_APP_CONNECTION = "delete-app-connection"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@ -1206,6 +1227,117 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignSshKey {
|
||||||
|
type: EventType.SIGN_SSH_KEY;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueSshCreds {
|
||||||
|
type: EventType.ISSUE_SSH_CREDS;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSshCa {
|
||||||
|
type: EventType.CREATE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCa {
|
||||||
|
type: EventType.GET_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshCa {
|
||||||
|
type: EventType.UPDATE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
status: SshCaStatus;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshCa {
|
||||||
|
type: EventType.DELETE_SSH_CA;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCaCertificateTemplates {
|
||||||
|
type: EventType.GET_SSH_CA_CERTIFICATE_TEMPLATES;
|
||||||
|
metadata: {
|
||||||
|
sshCaId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSshCertificateTemplate {
|
||||||
|
type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshCertificateTemplate {
|
||||||
|
type: EventType.GET_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSshCertificateTemplate {
|
||||||
|
type: EventType.UPDATE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
status: SshCertTemplateStatus;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshCertificateTemplate {
|
||||||
|
type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE;
|
||||||
|
metadata: {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCa {
|
interface CreateCa {
|
||||||
type: EventType.CREATE_CA;
|
type: EventType.CREATE_CA;
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -1742,6 +1874,39 @@ interface ApplyProjectTemplateEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetAppConnectionsEvent {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS;
|
||||||
|
metadata: {
|
||||||
|
app?: AppConnection;
|
||||||
|
count: number;
|
||||||
|
connectionIds: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetAppConnectionEvent {
|
||||||
|
type: EventType.GET_APP_CONNECTION;
|
||||||
|
metadata: {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateAppConnectionEvent {
|
||||||
|
type: EventType.CREATE_APP_CONNECTION;
|
||||||
|
metadata: Omit<TCreateAppConnectionDTO, "credentials"> & { connectionId: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateAppConnectionEvent {
|
||||||
|
type: EventType.UPDATE_APP_CONNECTION;
|
||||||
|
metadata: Omit<TUpdateAppConnectionDTO, "credentials"> & { connectionId: string; credentialsUpdated: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteAppConnectionEvent {
|
||||||
|
type: EventType.DELETE_APP_CONNECTION;
|
||||||
|
metadata: {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@ -1837,6 +2002,17 @@ export type Event =
|
|||||||
| SecretApprovalClosed
|
| SecretApprovalClosed
|
||||||
| SecretApprovalRequest
|
| SecretApprovalRequest
|
||||||
| SecretApprovalReopened
|
| SecretApprovalReopened
|
||||||
|
| SignSshKey
|
||||||
|
| IssueSshCreds
|
||||||
|
| CreateSshCa
|
||||||
|
| GetSshCa
|
||||||
|
| UpdateSshCa
|
||||||
|
| DeleteSshCa
|
||||||
|
| GetSshCaCertificateTemplates
|
||||||
|
| CreateSshCertificateTemplate
|
||||||
|
| UpdateSshCertificateTemplate
|
||||||
|
| GetSshCertificateTemplate
|
||||||
|
| DeleteSshCertificateTemplate
|
||||||
| CreateCa
|
| CreateCa
|
||||||
| GetCa
|
| GetCa
|
||||||
| UpdateCa
|
| UpdateCa
|
||||||
@ -1902,4 +2078,9 @@ export type Event =
|
|||||||
| CreateProjectTemplateEvent
|
| CreateProjectTemplateEvent
|
||||||
| UpdateProjectTemplateEvent
|
| UpdateProjectTemplateEvent
|
||||||
| DeleteProjectTemplateEvent
|
| DeleteProjectTemplateEvent
|
||||||
| ApplyProjectTemplateEvent;
|
| ApplyProjectTemplateEvent
|
||||||
|
| GetAppConnectionsEvent
|
||||||
|
| GetAppConnectionEvent
|
||||||
|
| CreateAppConnectionEvent
|
||||||
|
| UpdateAppConnectionEvent
|
||||||
|
| DeleteAppConnectionEvent;
|
||||||
|
@ -49,7 +49,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
},
|
},
|
||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false
|
projectTemplates: false,
|
||||||
|
appConnections: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
@ -67,6 +67,7 @@ export type TFeatureSet = {
|
|||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
projectTemplates: false;
|
projectTemplates: false;
|
||||||
|
appConnections: false; // TODO: remove once live
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
@ -27,7 +27,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
AdminConsole = "organization-admin-console",
|
AdminConsole = "organization-admin-console",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
ProjectTemplates = "project-templates"
|
ProjectTemplates = "project-templates",
|
||||||
|
AppConnections = "app-connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@ -46,6 +47,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
@ -123,6 +125,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
@ -153,6 +160,8 @@ const buildMemberPermission = () => {
|
|||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,6 +54,9 @@ export enum ProjectPermissionSub {
|
|||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates",
|
Certificates = "certificates",
|
||||||
CertificateTemplates = "certificate-templates",
|
CertificateTemplates = "certificate-templates",
|
||||||
|
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||||
|
SshCertificates = "ssh-certificates",
|
||||||
|
SshCertificateTemplates = "ssh-certificate-templates",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
@ -132,6 +135,9 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||||
@ -338,6 +344,28 @@ const GeneralPermissionSchema = [
|
|||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z
|
||||||
|
.literal(ProjectPermissionSub.SshCertificateAuthorities)
|
||||||
|
.describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SshCertificates).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z
|
||||||
|
.literal(ProjectPermissionSub.SshCertificateTemplates)
|
||||||
|
.describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
@ -480,7 +508,10 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.Certificates,
|
ProjectPermissionSub.Certificates,
|
||||||
ProjectPermissionSub.CertificateTemplates,
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
ProjectPermissionSub.PkiAlerts,
|
ProjectPermissionSub.PkiAlerts,
|
||||||
ProjectPermissionSub.PkiCollections
|
ProjectPermissionSub.PkiCollections,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities,
|
||||||
|
ProjectPermissionSub.SshCertificates,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
].forEach((el) => {
|
].forEach((el) => {
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
@ -665,6 +696,11 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
|
||||||
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||||
|
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||||
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionCmekActions.Create,
|
ProjectPermissionCmekActions.Create,
|
||||||
@ -707,6 +743,9 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateTemplateDALFactory = ReturnType<typeof sshCertificateTemplateDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateTemplateDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCertificateTemplateOrm = ormify(db, TableName.SshCertificateTemplate);
|
||||||
|
|
||||||
|
const getById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
|
||||||
|
.join(
|
||||||
|
TableName.SshCertificateAuthority,
|
||||||
|
`${TableName.SshCertificateAuthority}.id`,
|
||||||
|
`${TableName.SshCertificateTemplate}.sshCaId`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`)
|
||||||
|
.where(`${TableName.SshCertificateTemplate}.id`, "=", id)
|
||||||
|
.select(selectAllTableCols(TableName.SshCertificateTemplate))
|
||||||
|
.select(
|
||||||
|
db.ref("projectId").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get SSH certificate template by ID" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SSH certificate template named [name] within project with id [projectId]
|
||||||
|
*/
|
||||||
|
const getByName = async (name: string, projectId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const certTemplate = await (tx || db.replicaNode())(TableName.SshCertificateTemplate)
|
||||||
|
.join(
|
||||||
|
TableName.SshCertificateAuthority,
|
||||||
|
`${TableName.SshCertificateAuthority}.id`,
|
||||||
|
`${TableName.SshCertificateTemplate}.sshCaId`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.SshCertificateAuthority}.projectId`)
|
||||||
|
.where(`${TableName.SshCertificateTemplate}.name`, "=", name)
|
||||||
|
.where(`${TableName.Project}.id`, "=", projectId)
|
||||||
|
.select(selectAllTableCols(TableName.SshCertificateTemplate))
|
||||||
|
.select(
|
||||||
|
db.ref("projectId").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("friendlyName").as("caName").withSchema(TableName.SshCertificateAuthority),
|
||||||
|
db.ref("status").as("caStatus").withSchema(TableName.SshCertificateAuthority)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get SSH certificate template by name" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...sshCertificateTemplateOrm, getById, getByName };
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { SshCertificateTemplatesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCertificateTemplate = SshCertificateTemplatesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
sshCaId: true,
|
||||||
|
status: true,
|
||||||
|
name: true,
|
||||||
|
ttl: true,
|
||||||
|
maxTTL: true,
|
||||||
|
allowedUsers: true,
|
||||||
|
allowedHosts: true,
|
||||||
|
allowCustomKeyIds: true,
|
||||||
|
allowUserCertificates: true,
|
||||||
|
allowHostCertificates: true
|
||||||
|
});
|
@ -0,0 +1,249 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { ProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
|
||||||
|
import {
|
||||||
|
SshCertTemplateStatus,
|
||||||
|
TCreateSshCertTemplateDTO,
|
||||||
|
TDeleteSshCertTemplateDTO,
|
||||||
|
TGetSshCertTemplateDTO,
|
||||||
|
TUpdateSshCertTemplateDTO
|
||||||
|
} from "./ssh-certificate-template-types";
|
||||||
|
|
||||||
|
type TSshCertificateTemplateServiceFactoryDep = {
|
||||||
|
sshCertificateTemplateDAL: Pick<
|
||||||
|
TSshCertificateTemplateDALFactory,
|
||||||
|
"transaction" | "getByName" | "create" | "updateById" | "deleteById" | "getById"
|
||||||
|
>;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshCertificateTemplateServiceFactory = ReturnType<typeof sshCertificateTemplateServiceFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateTemplateServiceFactory = ({
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
permissionService
|
||||||
|
}: TSshCertificateTemplateServiceFactoryDep) => {
|
||||||
|
const createSshCertTemplate = async ({
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshCertTemplateDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(sshCaId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH CA with ID ${sshCaId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ms(ttl) > ms(maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TTL cannot be greater than max TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
|
||||||
|
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, ca.projectId, tx);
|
||||||
|
if (existingTemplate) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH certificate template with name ${name} already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
status: SshCertTemplateStatus.ACTIVE
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificateTemplate: newCertificateTemplate, ca };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSshCertTemplate = async ({
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshCertTemplateDTO) => {
|
||||||
|
const certTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCertificateTemplate = await sshCertificateTemplateDAL.transaction(async (tx) => {
|
||||||
|
if (name) {
|
||||||
|
const existingTemplate = await sshCertificateTemplateDAL.getByName(name, certTemplate.projectId, tx);
|
||||||
|
if (existingTemplate && existingTemplate.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `SSH certificate template with name ${name} already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms(ttl || certTemplate.ttl) > ms(maxTTL || certTemplate.maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "TTL cannot be greater than max TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.updateById(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
ttl,
|
||||||
|
maxTTL,
|
||||||
|
allowUserCertificates,
|
||||||
|
allowHostCertificates,
|
||||||
|
allowedUsers,
|
||||||
|
allowedHosts,
|
||||||
|
allowCustomKeyIds
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplate: updatedCertificateTemplate,
|
||||||
|
projectId: certTemplate.projectId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSshCertTemplate = async ({
|
||||||
|
id,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TDeleteSshCertTemplateDTO) => {
|
||||||
|
const certificateTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certificateTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateTemplateDAL.deleteById(certificateTemplate.id);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshCertTemplateDTO) => {
|
||||||
|
const certTemplate = await sshCertificateTemplateDAL.getById(id);
|
||||||
|
if (!certTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH certificate template with ID ${id} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
certTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
return certTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSshCertTemplate,
|
||||||
|
updateSshCertTemplate,
|
||||||
|
deleteSshCertTemplate,
|
||||||
|
getSshCertTemplate
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum SshCertTemplateStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateSshCertTemplateDTO = {
|
||||||
|
sshCaId: string;
|
||||||
|
name: string;
|
||||||
|
ttl: string;
|
||||||
|
maxTTL: string;
|
||||||
|
allowUserCertificates: boolean;
|
||||||
|
allowHostCertificates: boolean;
|
||||||
|
allowedUsers: string[];
|
||||||
|
allowedHosts: string[];
|
||||||
|
allowCustomKeyIds: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
status?: SshCertTemplateStatus;
|
||||||
|
name?: string;
|
||||||
|
ttl?: string;
|
||||||
|
maxTTL?: string;
|
||||||
|
allowUserCertificates?: boolean;
|
||||||
|
allowHostCertificates?: boolean;
|
||||||
|
allowedUsers?: string[];
|
||||||
|
allowedHosts?: string[];
|
||||||
|
allowCustomKeyIds?: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshCertTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
@ -0,0 +1,14 @@
|
|||||||
|
// Validates usernames or wildcard (*)
|
||||||
|
export const isValidUserPattern = (value: string): boolean => {
|
||||||
|
// Matches valid Linux usernames or a wildcard (*)
|
||||||
|
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
||||||
|
return userRegex.test(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validates hostnames, wildcard domains, or IP addresses
|
||||||
|
export const isValidHostPattern = (value: string): boolean => {
|
||||||
|
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
||||||
|
const hostRegex =
|
||||||
|
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
||||||
|
return hostRegex.test(value);
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateBodyDALFactory = ReturnType<typeof sshCertificateBodyDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateBodyDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCertificateBodyOrm = ormify(db, TableName.SshCertificateBody);
|
||||||
|
return sshCertificateBodyOrm;
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateDALFactory = ReturnType<typeof sshCertificateDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCertificateOrm = ormify(db, TableName.SshCertificate);
|
||||||
|
|
||||||
|
const countSshCertificatesInProject = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = db
|
||||||
|
.replicaNode()(TableName.SshCertificate)
|
||||||
|
.join(
|
||||||
|
TableName.SshCertificateAuthority,
|
||||||
|
`${TableName.SshCertificate}.sshCaId`,
|
||||||
|
`${TableName.SshCertificateAuthority}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.SshCertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.where(`${TableName.Project}.id`, projectId);
|
||||||
|
|
||||||
|
const count = await query.count("*").first();
|
||||||
|
|
||||||
|
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Count all SSH certificates in project" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...sshCertificateOrm,
|
||||||
|
countSshCertificatesInProject
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import { SshCertificatesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCertificate = SshCertificatesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
sshCaId: true,
|
||||||
|
sshCertificateTemplateId: true,
|
||||||
|
serialNumber: true,
|
||||||
|
certType: true,
|
||||||
|
publicKey: true,
|
||||||
|
principals: true,
|
||||||
|
keyId: true,
|
||||||
|
notBefore: true,
|
||||||
|
notAfter: true
|
||||||
|
});
|
10
backend/src/ee/services/ssh/ssh-certificate-authority-dal.ts
Normal file
10
backend/src/ee/services/ssh/ssh-certificate-authority-dal.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorityDALFactory = ReturnType<typeof sshCertificateAuthorityDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthorityDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCaOrm = ormify(db, TableName.SshCertificateAuthority);
|
||||||
|
return sshCaOrm;
|
||||||
|
};
|
376
backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts
Normal file
376
backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
import { execFile } from "child_process";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import ms from "ms";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isValidHostPattern,
|
||||||
|
isValidUserPattern
|
||||||
|
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
|
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
export const createSshCertSerialNumber = () => {
|
||||||
|
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||||
|
randomBytes[0] &= 0x7f; // Ensure the most significant bit is 0 (to stay within unsigned range)
|
||||||
|
return BigInt(`0x${randomBytes.toString("hex")}`).toString(10); // Convert to decimal
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: 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;
|
||||||
|
|
||||||
|
switch (keyAlgorithm) {
|
||||||
|
case CertKeyAlgorithm.RSA_2048:
|
||||||
|
keyType = "rsa";
|
||||||
|
keyBits = "2048";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.RSA_4096:
|
||||||
|
keyType = "rsa";
|
||||||
|
keyBits = "4096";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.ECDSA_P256:
|
||||||
|
keyType = "ecdsa";
|
||||||
|
keyBits = "256";
|
||||||
|
break;
|
||||||
|
case CertKeyAlgorithm.ECDSA_P384:
|
||||||
|
keyType = "ecdsa";
|
||||||
|
keyBits = "384";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate the SSH key pair
|
||||||
|
// The "-N ''" sets an empty passphrase
|
||||||
|
// The keys are created in the temporary directory
|
||||||
|
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]);
|
||||||
|
|
||||||
|
// Read the generated keys
|
||||||
|
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
||||||
|
const privateKey = await fs.readFile(privateKeyFile, "utf8");
|
||||||
|
|
||||||
|
return { publicKey, privateKey };
|
||||||
|
} finally {
|
||||||
|
// Cleanup the temporary directory and all its contents
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the SSH public key for the given SSH private key.
|
||||||
|
*/
|
||||||
|
export const getSshPublicKey = async (privateKey: string) => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||||
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
|
try {
|
||||||
|
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
||||||
|
|
||||||
|
// Run ssh-keygen to extract the public key
|
||||||
|
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
|
||||||
|
return stdout.trim();
|
||||||
|
} finally {
|
||||||
|
// Ensure that files and the temporary directory are cleaned up
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate type based on the SSH certificate template configuration.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateType = (template: TSshCertificateTemplates, certType: SshCertType) => {
|
||||||
|
if (!template.allowUserCertificates && certType === SshCertType.USER) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate user certificate type due to template restriction" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!template.allowHostCertificates && certType === SshCertType.HOST) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate host certificate type due to template restriction" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate principals based on the SSH certificate template configuration.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificatePrincipals = (
|
||||||
|
certType: SshCertType,
|
||||||
|
template: TSshCertificateTemplates,
|
||||||
|
principals: string[]
|
||||||
|
) => {
|
||||||
|
/**
|
||||||
|
* Validate and sanitize a principal string
|
||||||
|
*/
|
||||||
|
const validatePrincipal = (principal: string) => {
|
||||||
|
const sanitized = principal.trim();
|
||||||
|
|
||||||
|
// basic checks for empty or control characters
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Principal cannot be an empty string."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\r|\n|\t|\0/.test(sanitized)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' contains invalid whitespace or control characters.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow whitespace anywhere
|
||||||
|
if (/\s/.test(sanitized)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' cannot contain whitespace.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
|
||||||
|
if (!/^[A-Za-z0-9._-]+$/.test(sanitized)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// disallow leading hyphen to avoid potential argument-like inputs
|
||||||
|
if (sanitized.startsWith("-")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' cannot start with a hyphen.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// length restriction (adjust as needed)
|
||||||
|
if (sanitized.length > 64) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${sanitized}' is too long.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanitize and validate all principals using the helper
|
||||||
|
const sanitizedPrincipals = principals.map(validatePrincipal);
|
||||||
|
|
||||||
|
switch (certType) {
|
||||||
|
case SshCertType.USER: {
|
||||||
|
if (template.allowedUsers.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "No allowed users are configured in the SSH certificate template."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowsAllUsers = template.allowedUsers.includes("*") ?? false;
|
||||||
|
|
||||||
|
sanitizedPrincipals.forEach((principal) => {
|
||||||
|
if (principal === "*") {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '*' is not allowed for user certificates.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (allowsAllUsers && !isValidUserPattern(principal)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' does not match a valid user pattern.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!allowsAllUsers && !template.allowedUsers.includes(principal)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' is not in the list of allowed users.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SshCertType.HOST: {
|
||||||
|
if (template.allowedHosts.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "No allowed hosts are configured in the SSH certificate template."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowsAllHosts = template.allowedHosts.includes("*") ?? false;
|
||||||
|
|
||||||
|
sanitizedPrincipals.forEach((principal) => {
|
||||||
|
if (principal.includes("*")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' with wildcards is not allowed for host certificates.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (allowsAllHosts && !isValidHostPattern(principal)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' does not match a valid host pattern.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allowsAllHosts &&
|
||||||
|
!template.allowedHosts.some((allowedHost) => {
|
||||||
|
if (allowedHost.startsWith("*.")) {
|
||||||
|
const baseDomain = allowedHost.slice(2); // Remove the leading "*."
|
||||||
|
return principal.endsWith(`.${baseDomain}`);
|
||||||
|
}
|
||||||
|
return principal === allowedHost;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Principal '${principal}' is not in the list of allowed hosts or domains.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH certificate principals due to unrecognized requested certificate type"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate TTL based on the SSH certificate template configuration.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateTtl = (template: TSshCertificateTemplates, ttl?: string) => {
|
||||||
|
if (!ttl) {
|
||||||
|
// use default template ttl
|
||||||
|
return Math.ceil(ms(template.ttl) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms(ttl) > ms(template.maxTTL)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed TTL validation due to TTL being greater than configured max TTL on template"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(ms(ttl) / 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the requested SSH certificate key ID to ensure
|
||||||
|
* that it only contains alphanumeric characters with no spaces.
|
||||||
|
*/
|
||||||
|
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||||
|
const regex = /^[A-Za-z0-9-]+$/;
|
||||||
|
if (!regex.test(keyId)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyId.length > 50) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "keyId can only be up to 50 characters long."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the format of the SSH public key
|
||||||
|
*/
|
||||||
|
const validateSshPublicKey = async (publicKey: string) => {
|
||||||
|
const validPrefixes = ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"];
|
||||||
|
const startsWithValidPrefix = validPrefixes.some((prefix) => publicKey.startsWith(`${prefix} `));
|
||||||
|
if (!startsWithValidPrefix) {
|
||||||
|
throw new BadRequestError({ message: "Failed to validate SSH public key format: unsupported key type." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the key to a temp file and run `ssh-keygen -l -f`
|
||||||
|
// check to see if OpenSSH can read/interpret the public key
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-pubkey-"));
|
||||||
|
const pubKeyFile = path.join(tempDir, "key.pub");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
|
||||||
|
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile]);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH public key format: could not be parsed."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an SSH certificate for a user or host.
|
||||||
|
*/
|
||||||
|
export const createSshCert = async ({
|
||||||
|
template,
|
||||||
|
caPrivateKey,
|
||||||
|
clientPublicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl,
|
||||||
|
certType
|
||||||
|
}: TCreateSshCertDTO) => {
|
||||||
|
// 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
|
||||||
|
const ttl = validateSshCertificateTtl(template, requestedTtl);
|
||||||
|
|
||||||
|
validateSshCertificateKeyId(keyId);
|
||||||
|
await validateSshPublicKey(clientPublicKey);
|
||||||
|
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-cert-"));
|
||||||
|
|
||||||
|
const publicKeyFile = path.join(tempDir, "user_key.pub");
|
||||||
|
const privateKeyFile = path.join(tempDir, "ca_key");
|
||||||
|
const signedPublicKeyFile = path.join(tempDir, "user_key-cert.pub");
|
||||||
|
|
||||||
|
const serialNumber = createSshCertSerialNumber();
|
||||||
|
|
||||||
|
// Build `ssh-keygen` arguments for signing
|
||||||
|
// Using an array avoids shell injection issues
|
||||||
|
const sshKeygenArgs = [
|
||||||
|
certType === "host" ? "-h" : null, // host certificate if needed
|
||||||
|
"-s",
|
||||||
|
privateKeyFile, // path to SSH CA private key
|
||||||
|
"-I",
|
||||||
|
keyId, // identity (key ID)
|
||||||
|
"-n",
|
||||||
|
principals.join(","), // principals
|
||||||
|
"-V",
|
||||||
|
`+${ttl}s`, // validity (TTL in seconds)
|
||||||
|
"-z",
|
||||||
|
serialNumber, // serial number
|
||||||
|
publicKeyFile // public key file to sign
|
||||||
|
].filter(Boolean) as string[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write public and private keys to the temp directory
|
||||||
|
await fs.writeFile(publicKeyFile, clientPublicKey, { mode: 0o600 });
|
||||||
|
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
||||||
|
|
||||||
|
// Execute the signing process
|
||||||
|
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
|
||||||
|
|
||||||
|
// Read the signed public key from the generated cert file
|
||||||
|
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
||||||
|
|
||||||
|
return { serialNumber, signedPublicKey, ttl };
|
||||||
|
} finally {
|
||||||
|
// Cleanup the temporary directory and all its contents
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { SshCertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
projectId: true,
|
||||||
|
friendlyName: true,
|
||||||
|
status: true,
|
||||||
|
keyAlgorithm: true
|
||||||
|
});
|
@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshCertificateAuthoritySecretDALFactory = ReturnType<typeof sshCertificateAuthoritySecretDALFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthoritySecretDALFactory = (db: TDbClient) => {
|
||||||
|
const sshCaSecretOrm = ormify(db, TableName.SshCertificateAuthoritySecret);
|
||||||
|
return sshCaSecretOrm;
|
||||||
|
};
|
523
backend/src/ee/services/ssh/ssh-certificate-authority-service.ts
Normal file
523
backend/src/ee/services/ssh/ssh-certificate-authority-service.ts
Normal file
@ -0,0 +1,523 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, 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 { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
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 { createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
||||||
|
import {
|
||||||
|
SshCaStatus,
|
||||||
|
TCreateSshCaDTO,
|
||||||
|
TDeleteSshCaDTO,
|
||||||
|
TGetSshCaCertificateTemplatesDTO,
|
||||||
|
TGetSshCaDTO,
|
||||||
|
TGetSshCaPublicKeyDTO,
|
||||||
|
TIssueSshCredsDTO,
|
||||||
|
TSignSshKeyDTO,
|
||||||
|
TUpdateSshCaDTO
|
||||||
|
} from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
|
type TSshCertificateAuthorityServiceFactoryDep = {
|
||||||
|
sshCertificateAuthorityDAL: Pick<
|
||||||
|
TSshCertificateAuthorityDALFactory,
|
||||||
|
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
|
||||||
|
>;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||||
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find" | "getById">;
|
||||||
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||||
|
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||||
|
kmsService: Pick<
|
||||||
|
TKmsServiceFactory,
|
||||||
|
"generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "getOrgKmsKeyId" | "createCipherPairWithDataKey"
|
||||||
|
>;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshCertificateAuthorityServiceFactory = ReturnType<typeof sshCertificateAuthorityServiceFactory>;
|
||||||
|
|
||||||
|
export const sshCertificateAuthorityServiceFactory = ({
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
}: TSshCertificateAuthorityServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Generates a new SSH CA
|
||||||
|
*/
|
||||||
|
const createSshCa = async ({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
keyAlgorithm,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshCaDTO) => {
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TGetSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: ca.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return { ...ca, publicKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public key of SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getSshCaPublicKey = async ({ caId }: TGetSshCaPublicKeyDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: ca.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update SSH CA with id [caId]
|
||||||
|
* Note: Used to enable/disable CA
|
||||||
|
*/
|
||||||
|
const updateSshCaById = async ({
|
||||||
|
caId,
|
||||||
|
friendlyName,
|
||||||
|
status,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCa = await sshCertificateAuthorityDAL.updateById(caId, { friendlyName, status });
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: ca.id });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: ca.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
|
return { ...updatedCa, publicKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete SSH CA with id [caId]
|
||||||
|
*/
|
||||||
|
const deleteSshCaById = async ({ caId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteSshCaDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCa = await sshCertificateAuthorityDAL.deleteById(caId);
|
||||||
|
|
||||||
|
return deletedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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].
|
||||||
|
*/
|
||||||
|
const issueSshCreds = async ({
|
||||||
|
certificateTemplateId,
|
||||||
|
keyAlgorithm,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
ttl: requestedTtl,
|
||||||
|
keyId: requestedKeyId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueSshCredsDTO) => {
|
||||||
|
const sshCertificateTemplate = await sshCertificateTemplateDAL.getById(certificateTemplateId);
|
||||||
|
if (!sshCertificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "No SSH certificate template found with specified name"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
sshCertificateTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH CA is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH certificate template is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
|
? requestedKeyId ?? `${actor}-${actorId}`
|
||||||
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// create user key pair
|
||||||
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
|
template: sshCertificateTemplate,
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
clientPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl,
|
||||||
|
certType
|
||||||
|
});
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedCertificate = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
|
await sshCertificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await sshCertificateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: sshCertificateTemplate.sshCaId,
|
||||||
|
sshCertificateTemplateId: sshCertificateTemplate.id,
|
||||||
|
serialNumber,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
keyId,
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(Date.now() + ttl * 1000)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
sshCertId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
certificateTemplate: sshCertificateTemplate,
|
||||||
|
ttl,
|
||||||
|
keyId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH certificate by signing SSH public key [publicKey]
|
||||||
|
* using CA behind SSH certificate template with name [templateName]
|
||||||
|
*/
|
||||||
|
const signSshKey = async ({
|
||||||
|
certificateTemplateId,
|
||||||
|
publicKey,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
ttl: requestedTtl,
|
||||||
|
keyId: requestedKeyId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TSignSshKeyDTO) => {
|
||||||
|
const sshCertificateTemplate = await sshCertificateTemplateDAL.getById(certificateTemplateId);
|
||||||
|
if (!sshCertificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "No SSH certificate template found with specified name"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
sshCertificateTemplate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SshCertificates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.caStatus === SshCaStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH CA is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sshCertificateTemplate.status === SshCertTemplateStatus.DISABLED) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "SSH certificate template is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set [keyId] depending on if [allowCustomKeyIds] is true or false
|
||||||
|
const keyId = sshCertificateTemplate.allowCustomKeyIds
|
||||||
|
? requestedKeyId ?? `${actor}-${actorId}`
|
||||||
|
: `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: sshCertificateTemplate.sshCaId });
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaPrivateKey = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
|
template: sshCertificateTemplate,
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
clientPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl,
|
||||||
|
certType
|
||||||
|
});
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: sshCertificateTemplate.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedCertificate = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(signedPublicKey, "utf8")
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
|
await sshCertificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await sshCertificateDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: sshCertificateTemplate.sshCaId,
|
||||||
|
sshCertificateTemplateId: sshCertificateTemplate.id,
|
||||||
|
serialNumber,
|
||||||
|
certType,
|
||||||
|
principals,
|
||||||
|
keyId,
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(Date.now() + ttl * 1000)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshCertificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
sshCertId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { serialNumber, signedPublicKey, certificateTemplate: sshCertificateTemplate, ttl, keyId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshCaCertificateTemplates = async ({
|
||||||
|
caId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetSshCaCertificateTemplatesDTO) => {
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` });
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const certificateTemplates = await sshCertificateTemplateDAL.find({ sshCaId: caId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateTemplates,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueSshCreds,
|
||||||
|
signSshKey,
|
||||||
|
createSshCa,
|
||||||
|
getSshCaById,
|
||||||
|
getSshCaPublicKey,
|
||||||
|
updateSshCaById,
|
||||||
|
deleteSshCaById,
|
||||||
|
getSshCaCertificateTemplates
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
export enum SshCaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SshCertType {
|
||||||
|
USER = "user",
|
||||||
|
HOST = "host"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateSshCaDTO = {
|
||||||
|
friendlyName: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshCaPublicKeyDTO = {
|
||||||
|
caId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
status?: SshCaStatus;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueSshCredsDTO = {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl?: string;
|
||||||
|
keyId?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TSignSshKeyDTO = {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
publicKey: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
principals: string[];
|
||||||
|
ttl?: string;
|
||||||
|
keyId?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshCaCertificateTemplatesDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TCreateSshCertDTO = {
|
||||||
|
template: TSshCertificateTemplates;
|
||||||
|
caPrivateKey: string;
|
||||||
|
clientPublicKey: string;
|
||||||
|
keyId: string;
|
||||||
|
principals: string[];
|
||||||
|
requestedTtl?: string;
|
||||||
|
certType: SshCertType;
|
||||||
|
};
|
@ -1,3 +1,6 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
|
||||||
export const GROUPS = {
|
export const GROUPS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
name: "The name of the group to create.",
|
name: "The name of the group to create.",
|
||||||
@ -492,6 +495,17 @@ export const PROJECTS = {
|
|||||||
LIST_INTEGRATION_AUTHORIZATION: {
|
LIST_INTEGRATION_AUTHORIZATION: {
|
||||||
workspaceId: "The ID of the project to list integration auths for."
|
workspaceId: "The ID of the project to list integration auths for."
|
||||||
},
|
},
|
||||||
|
LIST_SSH_CAS: {
|
||||||
|
projectId: "The ID of the project to list SSH CAs 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.",
|
||||||
|
limit: "The number of SSH certificates to return."
|
||||||
|
},
|
||||||
|
LIST_SSH_CERTIFICATE_TEMPLATES: {
|
||||||
|
projectId: "The ID of the project to list SSH certificate templates for."
|
||||||
|
},
|
||||||
LIST_CAS: {
|
LIST_CAS: {
|
||||||
slug: "The slug of the project to list CAs for.",
|
slug: "The slug of the project to list CAs for.",
|
||||||
status: "The status of the CA to filter by.",
|
status: "The status of the CA to filter by.",
|
||||||
@ -1126,6 +1140,7 @@ export const INTEGRATION = {
|
|||||||
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
||||||
secretGCPLabel: "The label for GCP secrets.",
|
secretGCPLabel: "The label for GCP secrets.",
|
||||||
secretAWSTag: "The tags for AWS secrets.",
|
secretAWSTag: "The tags for AWS secrets.",
|
||||||
|
azureLabel: "Define which label to assign to secrets created in Azure App Configuration.",
|
||||||
githubVisibility:
|
githubVisibility:
|
||||||
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
|
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
|
||||||
githubVisibilityRepoIds:
|
githubVisibilityRepoIds:
|
||||||
@ -1186,6 +1201,84 @@ export const AUDIT_LOG_STREAMS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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."
|
||||||
|
},
|
||||||
|
GET: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get."
|
||||||
|
},
|
||||||
|
GET_PUBLIC_KEY: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get the public key for."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to update.",
|
||||||
|
friendlyName: "A friendly name for the SSH CA to update to.",
|
||||||
|
status: "The status of the SSH CA to update to. This can be one of active or disabled."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to delete."
|
||||||
|
},
|
||||||
|
GET_CERTIFICATE_TEMPLATES: {
|
||||||
|
sshCaId: "The ID of the SSH CA to get the certificate templates for."
|
||||||
|
},
|
||||||
|
SIGN_SSH_KEY: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to sign the SSH public key with.",
|
||||||
|
publicKey: "The SSH public key to sign.",
|
||||||
|
certType: "The type of certificate to issue. This can be one of user or host.",
|
||||||
|
principals: "The list of principals (usernames, hostnames) to include in the certificate.",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.",
|
||||||
|
keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.",
|
||||||
|
serialNumber: "The serial number of the issued SSH certificate.",
|
||||||
|
signedKey: "The SSH certificate or signed SSH public key."
|
||||||
|
},
|
||||||
|
ISSUE_SSH_CREDENTIALS: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to issue the SSH credentials with.",
|
||||||
|
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA.",
|
||||||
|
certType: "The type of certificate to issue. This can be one of user or host.",
|
||||||
|
principals: "The list of principals (usernames, hostnames) to include in the certificate.",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, ... If not specified, the default TTL for the template will be used.",
|
||||||
|
keyId: "The key ID to include in the certificate. If not specified, a default key ID will be generated.",
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SSH_CERTIFICATE_TEMPLATES = {
|
||||||
|
GET: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to get."
|
||||||
|
},
|
||||||
|
CREATE: {
|
||||||
|
sshCaId: "The ID of the SSH CA to associate the certificate template with.",
|
||||||
|
name: "The name of the certificate template.",
|
||||||
|
ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
allowedUsers: "The list of allowed users for certificates issued under this template.",
|
||||||
|
allowedHosts: "The list of allowed hosts for certificates issued under this template.",
|
||||||
|
allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.",
|
||||||
|
allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.",
|
||||||
|
allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to update.",
|
||||||
|
name: "The name of the certificate template.",
|
||||||
|
ttl: "The default time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
maxTTL: "The maximum time to live for issued certificates such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
allowedUsers: "The list of allowed users for certificates issued under this template.",
|
||||||
|
allowedHosts: "The list of allowed hosts for certificates issued under this template.",
|
||||||
|
allowUserCertificates: "Whether or not to allow user certificates to be issued under this template.",
|
||||||
|
allowHostCertificates: "Whether or not to allow host certificates to be issued under this template.",
|
||||||
|
allowCustomKeyIds: "Whether or not to allow custom key IDs for certificates issued under this template."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
certificateTemplateId: "The ID of the SSH certificate template to delete."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES = {
|
export const CERTIFICATE_AUTHORITIES = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectSlug: "Slug of the project to create the CA in.",
|
projectSlug: "Slug of the project to create the CA in.",
|
||||||
@ -1515,3 +1608,34 @@ export const ProjectTemplates = {
|
|||||||
templateId: "The ID of the project template to be deleted."
|
templateId: "The ID of the project template to be deleted."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AppConnections = {
|
||||||
|
GET_BY_ID: (app: AppConnection) => ({
|
||||||
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||||
|
}),
|
||||||
|
GET_BY_NAME: (app: AppConnection) => ({
|
||||||
|
connectionName: `The name of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
|
||||||
|
}),
|
||||||
|
CREATE: (app: AppConnection) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
return {
|
||||||
|
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||||
|
description: `An optional description for the ${appName} Connection.`,
|
||||||
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
|
method: `The method used to authenticate with ${appName}.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
UPDATE: (app: AppConnection) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
return {
|
||||||
|
connectionId: `The ID of the ${appName} Connection to be updated.`,
|
||||||
|
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||||
|
description: `The updated description of the ${appName} Connection.`,
|
||||||
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
|
method: `The method used to authenticate with ${appName}.`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
DELETE: (app: AppConnection) => ({
|
||||||
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
@ -180,7 +180,24 @@ const envSchema = z
|
|||||||
HSM_SLOT: z.coerce.number().optional().default(0),
|
HSM_SLOT: z.coerce.number().optional().default(0),
|
||||||
|
|
||||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false")
|
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||||
|
|
||||||
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// aws
|
||||||
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// github oauth
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// github app
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional())
|
||||||
})
|
})
|
||||||
// To ensure that basic encryption is always possible.
|
// To ensure that basic encryption is always possible.
|
||||||
.refine(
|
.refine(
|
||||||
|
@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => {
|
|||||||
if (str.startsWith("/")) return str;
|
if (str.startsWith("/")) return str;
|
||||||
return `/${str}`;
|
return `/${str}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
|
||||||
|
@ -20,11 +20,12 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
|||||||
|
|
||||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||||
|
$notNull?: Array<keyof R>;
|
||||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||||
$complex?: TKnexDynamicOperator<R>;
|
$complex?: TKnexDynamicOperator<R>;
|
||||||
};
|
};
|
||||||
export const buildFindFilter =
|
export const buildFindFilter =
|
||||||
<R extends object = object>({ $in, $search, $complex, ...filter }: TFindFilter<R>) =>
|
<R extends object = object>({ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>) =>
|
||||||
(bd: Knex.QueryBuilder<R, R>) => {
|
(bd: Knex.QueryBuilder<R, R>) => {
|
||||||
void bd.where(filter);
|
void bd.where(filter);
|
||||||
if ($in) {
|
if ($in) {
|
||||||
@ -34,6 +35,13 @@ export const buildFindFilter =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($notNull?.length) {
|
||||||
|
$notNull.forEach((key) => {
|
||||||
|
void bd.whereNotNull(key as never);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
Object.entries($search).forEach(([key, val]) => {
|
Object.entries($search).forEach(([key, val]) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
|
@ -43,6 +43,8 @@ export type RequiredKeys<T> = {
|
|||||||
|
|
||||||
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||||
|
|
||||||
|
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
||||||
|
|
||||||
export enum EnforcementLevel {
|
export enum EnforcementLevel {
|
||||||
Hard = "hard",
|
Hard = "hard",
|
||||||
Soft = "soft"
|
Soft = "soft"
|
||||||
|
@ -317,6 +317,13 @@ export const queueServiceFactory = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRepeatableJobs = (name: QueueName, startOffset?: number, endOffset?: number) => {
|
||||||
|
const q = queueContainer[name];
|
||||||
|
if (!q) throw new Error(`Queue '${name}' not initialized`);
|
||||||
|
|
||||||
|
return q.getRepeatableJobs(startOffset, endOffset);
|
||||||
|
};
|
||||||
|
|
||||||
const stopRepeatableJobByJobId = async <T extends QueueName>(name: T, jobId: string) => {
|
const stopRepeatableJobByJobId = async <T extends QueueName>(name: T, jobId: string) => {
|
||||||
const q = queueContainer[name];
|
const q = queueContainer[name];
|
||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
@ -326,6 +333,11 @@ export const queueServiceFactory = (
|
|||||||
return q.removeRepeatableByKey(job.repeatJobKey);
|
return q.removeRepeatableByKey(job.repeatJobKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopRepeatableJobByKey = async <T extends QueueName>(name: T, repeatJobKey: string) => {
|
||||||
|
const q = queueContainer[name];
|
||||||
|
return q.removeRepeatableByKey(repeatJobKey);
|
||||||
|
};
|
||||||
|
|
||||||
const stopJobById = async <T extends QueueName>(name: T, jobId: string) => {
|
const stopJobById = async <T extends QueueName>(name: T, jobId: string) => {
|
||||||
const q = queueContainer[name];
|
const q = queueContainer[name];
|
||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
@ -349,8 +361,10 @@ export const queueServiceFactory = (
|
|||||||
shutdown,
|
shutdown,
|
||||||
stopRepeatableJob,
|
stopRepeatableJob,
|
||||||
stopRepeatableJobByJobId,
|
stopRepeatableJobByJobId,
|
||||||
|
stopRepeatableJobByKey,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
stopJobById,
|
stopJobById,
|
||||||
|
getRepeatableJobs,
|
||||||
startPg,
|
startPg,
|
||||||
queuePg
|
queuePg
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ForbiddenError, PureAbility } from "@casl/ability";
|
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||||
|
import opentelemetry from "@opentelemetry/api";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
@ -35,8 +37,30 @@ enum HttpStatusCodes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||||
|
const errorHistogram = apiMeter.createHistogram("API_errors", {
|
||||||
|
description: "API errors by type, status code, and name",
|
||||||
|
unit: "1"
|
||||||
|
});
|
||||||
|
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
const { method } = req;
|
||||||
|
const route = req.routerPath;
|
||||||
|
const errorType =
|
||||||
|
error instanceof jwt.JsonWebTokenError ? "TokenError" : error.constructor.name || "UnknownError";
|
||||||
|
|
||||||
|
errorHistogram.record(1, {
|
||||||
|
route,
|
||||||
|
method,
|
||||||
|
type: errorType,
|
||||||
|
name: error.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
void res
|
void res
|
||||||
.status(HttpStatusCodes.BadRequest)
|
.status(HttpStatusCodes.BadRequest)
|
||||||
@ -52,13 +76,20 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
message: error.message,
|
message: error.message,
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
} else if (error instanceof DatabaseError) {
|
||||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
statusCode: HttpStatusCodes.InternalServerError,
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
message: "Something went wrong",
|
message: "Something went wrong",
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
|
} else if (error instanceof InternalServerError) {
|
||||||
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
|
reqId: req.id,
|
||||||
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
|
message: error.message ?? "Something went wrong",
|
||||||
|
error: error.name
|
||||||
|
});
|
||||||
} else if (error instanceof GatewayTimeoutError) {
|
} else if (error instanceof GatewayTimeoutError) {
|
||||||
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
|
@ -75,6 +75,13 @@ import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-da
|
|||||||
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
|
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
|
||||||
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
||||||
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
import { snapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
||||||
|
import { sshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { sshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { sshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
|
import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
|
||||||
|
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 { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@ -84,6 +91,8 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||||
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||||
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||||
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||||
@ -307,6 +316,7 @@ export const registerRoutes = async (
|
|||||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
const telemetryDAL = telemetryDALFactory(db);
|
const telemetryDAL = telemetryDALFactory(db);
|
||||||
|
const appConnectionDAL = appConnectionDALFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
@ -345,6 +355,12 @@ export const registerRoutes = async (
|
|||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||||
|
|
||||||
|
const sshCertificateDAL = sshCertificateDALFactory(db);
|
||||||
|
const sshCertificateBodyDAL = sshCertificateBodyDALFactory(db);
|
||||||
|
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
||||||
|
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
||||||
|
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
||||||
|
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
const externalKmsDAL = externalKmsDALFactory(db);
|
const externalKmsDAL = externalKmsDALFactory(db);
|
||||||
@ -538,7 +554,11 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const orgService = orgServiceFactory({
|
const orgService = orgServiceFactory({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
|
queueService,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@ -559,6 +579,7 @@ export const registerRoutes = async (
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
oidcConfigDAL,
|
oidcConfigDAL,
|
||||||
|
loginService,
|
||||||
projectBotService
|
projectBotService
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
@ -707,6 +728,22 @@ export const registerRoutes = async (
|
|||||||
queueService
|
queueService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const sshCertificateTemplateService = sshCertificateTemplateServiceFactory({
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@ -776,10 +813,58 @@ export const registerRoutes = async (
|
|||||||
projectTemplateDAL
|
projectTemplateDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
|
integrationAuthDAL,
|
||||||
|
integrationDAL,
|
||||||
|
permissionService,
|
||||||
|
projectBotService,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretQueueService = secretQueueFactory({
|
||||||
|
keyStore,
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
integrationAuthService,
|
||||||
|
projectBotService,
|
||||||
|
integrationDAL,
|
||||||
|
secretImportDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
webhookDAL,
|
||||||
|
orgDAL,
|
||||||
|
auditLogService,
|
||||||
|
userDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
smtpService,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
kmsService,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
secretVersionTagV2BridgeDAL,
|
||||||
|
secretRotationDAL,
|
||||||
|
integrationAuthDAL,
|
||||||
|
snapshotDAL,
|
||||||
|
snapshotSecretV2BridgeDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
|
orgService
|
||||||
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
queueService,
|
||||||
projectQueue: projectQueueService,
|
projectQueue: projectQueueService,
|
||||||
|
projectBotService,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
@ -795,6 +880,9 @@ export const registerRoutes = async (
|
|||||||
certificateDAL,
|
certificateDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@ -859,48 +947,6 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
|
||||||
integrationAuthDAL,
|
|
||||||
integrationDAL,
|
|
||||||
permissionService,
|
|
||||||
projectBotService,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
const secretQueueService = secretQueueFactory({
|
|
||||||
keyStore,
|
|
||||||
queueService,
|
|
||||||
secretDAL,
|
|
||||||
folderDAL,
|
|
||||||
integrationAuthService,
|
|
||||||
projectBotService,
|
|
||||||
integrationDAL,
|
|
||||||
secretImportDAL,
|
|
||||||
projectEnvDAL,
|
|
||||||
webhookDAL,
|
|
||||||
orgDAL,
|
|
||||||
auditLogService,
|
|
||||||
userDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
smtpService,
|
|
||||||
projectDAL,
|
|
||||||
projectBotDAL,
|
|
||||||
secretVersionDAL,
|
|
||||||
secretBlindIndexDAL,
|
|
||||||
secretTagDAL,
|
|
||||||
secretVersionTagDAL,
|
|
||||||
kmsService,
|
|
||||||
secretVersionV2BridgeDAL,
|
|
||||||
secretV2BridgeDAL,
|
|
||||||
secretVersionTagV2BridgeDAL,
|
|
||||||
secretRotationDAL,
|
|
||||||
integrationAuthDAL,
|
|
||||||
snapshotDAL,
|
|
||||||
snapshotSecretV2BridgeDAL,
|
|
||||||
secretApprovalRequestDAL,
|
|
||||||
projectKeyDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
|
||||||
orgService
|
|
||||||
});
|
|
||||||
const secretImportService = secretImportServiceFactory({
|
const secretImportService = secretImportServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@ -1229,6 +1275,7 @@ export const registerRoutes = async (
|
|||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
|
secretDAL,
|
||||||
secretFolderVersionDAL: folderVersionDAL,
|
secretFolderVersionDAL: folderVersionDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
@ -1308,6 +1355,13 @@ export const registerRoutes = async (
|
|||||||
externalGroupOrgRoleMappingDAL
|
externalGroupOrgRoleMappingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appConnectionService = appConnectionServiceFactory({
|
||||||
|
appConnectionDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@ -1376,6 +1430,8 @@ export const registerRoutes = async (
|
|||||||
auditLog: auditLogService,
|
auditLog: auditLogService,
|
||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
certificate: certificateService,
|
certificate: certificateService,
|
||||||
|
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||||
|
sshCertificateTemplate: sshCertificateTemplateService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
@ -1402,7 +1458,8 @@ export const registerRoutes = async (
|
|||||||
migration: migrationService,
|
migration: migrationService,
|
||||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
||||||
projectTemplate: projectTemplateService,
|
projectTemplate: projectTemplateService,
|
||||||
totp: totpService
|
totp: totpService,
|
||||||
|
appConnection: appConnectionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
// can't use discriminated due to multiple schemas for certain apps
|
||||||
|
const SanitizedAppConnectionSchema = z.union([
|
||||||
|
...SanitizedAwsConnectionSchema.options,
|
||||||
|
...SanitizedGitHubConnectionSchema.options
|
||||||
|
]);
|
||||||
|
|
||||||
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
|
AwsConnectionListItemSchema,
|
||||||
|
GitHubConnectionListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/options",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List the available App Connection Options.",
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
appConnectionOptions: AppConnectionOptionsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: () => {
|
||||||
|
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
|
||||||
|
return { appConnectionOptions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List all the App Connections for the current organization.",
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
|
metadata: {
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnections };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,274 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { startsWithVowel } from "@app/lib/fn";
|
||||||
|
import { readLimit, writeLimit } 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 { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
|
||||||
|
server,
|
||||||
|
app,
|
||||||
|
createSchema,
|
||||||
|
updateSchema,
|
||||||
|
responseSchema
|
||||||
|
}: {
|
||||||
|
app: AppConnection;
|
||||||
|
server: FastifyZodProvider;
|
||||||
|
createSchema: z.ZodType<{
|
||||||
|
name: string;
|
||||||
|
method: I["method"];
|
||||||
|
credentials: I["credentials"];
|
||||||
|
description?: string | null;
|
||||||
|
}>;
|
||||||
|
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||||
|
responseSchema: z.ZodTypeAny;
|
||||||
|
}) => {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `List the ${appName} Connections for the current organization.`,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnections: responseSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
|
metadata: {
|
||||||
|
app,
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnections };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:connectionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${appName} Connection by ID.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.findAppConnectionById(
|
||||||
|
app,
|
||||||
|
connectionId,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/name/:connectionName`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${appName} Connection by name.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionName: z
|
||||||
|
.string()
|
||||||
|
.min(0, "Connection name required")
|
||||||
|
.describe(AppConnections.GET_BY_NAME(app).connectionName)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionName } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.findAppConnectionByName(
|
||||||
|
app,
|
||||||
|
connectionName,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId: appConnection.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Create ${
|
||||||
|
startsWithVowel(appName) ? "an" : "a"
|
||||||
|
} ${appName} Connection for the current organization.`,
|
||||||
|
body: createSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { name, method, credentials, description } = req.body;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||||
|
{ name, method, app, credentials, description },
|
||||||
|
req.permission
|
||||||
|
)) as TAppConnection;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
method,
|
||||||
|
app,
|
||||||
|
connectionId: appConnection.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:connectionId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Update the specified ${appName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.UPDATE(app).connectionId)
|
||||||
|
}),
|
||||||
|
body: updateSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { name, credentials, description } = req.body;
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.updateAppConnection(
|
||||||
|
{ name, credentials, connectionId, description },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
credentialsUpdated: Boolean(credentials),
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/:connectionId`,
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Delete the specified ${appName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appConnection: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const appConnection = (await server.services.appConnection.deleteAppConnection(
|
||||||
|
app,
|
||||||
|
connectionId,
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_APP_CONNECTION,
|
||||||
|
metadata: {
|
||||||
|
connectionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnection };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,17 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateAwsConnectionSchema,
|
||||||
|
SanitizedAwsConnectionSchema,
|
||||||
|
UpdateAwsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.AWS,
|
||||||
|
server,
|
||||||
|
responseSchema: SanitizedAwsConnectionSchema,
|
||||||
|
createSchema: CreateAwsConnectionSchema,
|
||||||
|
updateSchema: UpdateAwsConnectionSchema
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateGitHubConnectionSchema,
|
||||||
|
SanitizedGitHubConnectionSchema,
|
||||||
|
UpdateGitHubConnectionSchema
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerGitHubConnectionRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.GitHub,
|
||||||
|
server,
|
||||||
|
responseSchema: SanitizedGitHubConnectionSchema,
|
||||||
|
createSchema: CreateGitHubConnectionSchema,
|
||||||
|
updateSchema: UpdateGitHubConnectionSchema
|
||||||
|
});
|
@ -0,0 +1,8 @@
|
|||||||
|
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
|
||||||
|
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
|
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||||
|
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./app-connection-router";
|
||||||
|
export * from "./apps";
|
@ -1,3 +1,4 @@
|
|||||||
|
import { APP_CONNECTION_REGISTER_MAP, registerAppConnectionRouter } from "@app/server/routes/v1/app-connection-routers";
|
||||||
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
||||||
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
||||||
|
|
||||||
@ -110,4 +111,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||||
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (appConnectionsRouter) => {
|
||||||
|
await appConnectionsRouter.register(registerAppConnectionRouter);
|
||||||
|
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_MAP)) {
|
||||||
|
await appConnectionsRouter.register(router, { prefix: `/${app}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prefix: "/app-connections" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1185,4 +1185,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
return { spaces };
|
return { spaces };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:integrationAuthId/circleci/organizations",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
organizations: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
projects: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
contexts: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const organizations = await server.services.integrationAuth.getCircleCIOrganizations({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { organizations };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
|
|
||||||
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
@ -29,9 +30,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.extend({
|
organizations: sanitizedOrganizationSchema
|
||||||
orgAuthMethod: z.string()
|
.extend({
|
||||||
}).array()
|
orgAuthMethod: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -137,7 +137,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.default("false")
|
.default("false")
|
||||||
.transform((value) => value === "true"),
|
.transform((value) => value === "true"),
|
||||||
type: z.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, "all"]).optional()
|
type: z
|
||||||
|
.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, ProjectType.SSH, "all"])
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -363,21 +364,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organization: OrganizationsSchema
|
organization: OrganizationsSchema,
|
||||||
|
accessToken: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
handler: async (req, res) => {
|
||||||
if (req.auth.actor !== ActorType.USER) return;
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
const organization = await server.services.org.deleteOrganizationById(
|
const cfg = getConfig();
|
||||||
req.permission.id,
|
|
||||||
req.params.organizationId,
|
const { organization, tokens } = await server.services.org.deleteOrganizationById({
|
||||||
req.permission.authMethod,
|
userId: req.permission.id,
|
||||||
req.permission.orgId
|
orgId: req.params.organizationId,
|
||||||
);
|
actorAuthMethod: req.permission.authMethod,
|
||||||
return { organization };
|
actorOrgId: req.permission.orgId,
|
||||||
|
authorizationHeader: req.headers.authorization,
|
||||||
|
userAgentHeader: req.headers["user-agent"],
|
||||||
|
ipAddress: req.realIp
|
||||||
|
});
|
||||||
|
|
||||||
|
void res.setCookie("jid", tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: cfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organization, accessToken: tokens.accessToken };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,9 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
|
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 { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
@ -500,4 +503,101 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { certificateTemplates };
|
return { certificateTemplates };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().default(0).describe(PROJECTS.LIST_SSH_CERTIFICATES.offset),
|
||||||
|
limit: z.coerce.number().default(25).describe(PROJECTS.LIST_SSH_CERTIFICATES.limit)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificates: z.array(sanitizedSshCertificate),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificates, totalCount } = await server.services.project.listProjectSshCertificates({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
offset: req.query.offset,
|
||||||
|
limit: req.query.limit
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificates, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-certificate-templates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CERTIFICATE_TEMPLATES.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificateTemplates: z.array(sanitizedSshCertificateTemplate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificateTemplates } = await server.services.project.listProjectSshCertificateTemplates({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificateTemplates };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-cas",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
cas: z.array(sanitizedSshCa)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const cas = await server.services.project.listProjectSshCas({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { cas };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@ -134,7 +135,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return organizations that current user is part of",
|
description: "Return organizations that current user is part of",
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: sanitizedOrganizationSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
11
backend/src/services/app-connection/app-connection-dal.ts
Normal file
11
backend/src/services/app-connection/app-connection-dal.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
|
||||||
|
|
||||||
|
export const appConnectionDALFactory = (db: TDbClient) => {
|
||||||
|
const appConnectionOrm = ormify(db, TableName.AppConnection);
|
||||||
|
|
||||||
|
return { ...appConnectionOrm };
|
||||||
|
};
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum AppConnection {
|
||||||
|
GitHub = "github",
|
||||||
|
AWS = "aws"
|
||||||
|
}
|
92
backend/src/services/app-connection/app-connection-fns.ts
Normal file
92
backend/src/services/app-connection/app-connection-fns.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||||
|
import {
|
||||||
|
AwsConnectionMethod,
|
||||||
|
getAwsAppConnectionListItem,
|
||||||
|
validateAwsConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
getGitHubConnectionListItem,
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
validateGitHubConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
export const listAppConnectionOptions = () => {
|
||||||
|
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptAppConnectionCredentials = async ({
|
||||||
|
orgId,
|
||||||
|
credentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
credentials: TAppConnection["credentials"];
|
||||||
|
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify(credentials))
|
||||||
|
});
|
||||||
|
|
||||||
|
return encryptedCredentialsBlob;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptAppConnectionCredentials = async ({
|
||||||
|
orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
encryptedCredentials: Buffer;
|
||||||
|
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||||
|
}) => {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPlainTextBlob = decryptor({
|
||||||
|
cipherTextBlob: encryptedCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAppConnectionCredentials = async (
|
||||||
|
appConnection: TAppConnectionConfig
|
||||||
|
): Promise<TAppConnection["credentials"]> => {
|
||||||
|
const { app } = appConnection;
|
||||||
|
switch (app) {
|
||||||
|
case AppConnection.AWS: {
|
||||||
|
return validateAwsConnectionCredentials(appConnection);
|
||||||
|
}
|
||||||
|
case AppConnection.GitHub:
|
||||||
|
return validateGitHubConnectionCredentials(appConnection);
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled App Connection ${app}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||||
|
switch (method) {
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
return "GitHub App";
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
return "OAuth";
|
||||||
|
case AwsConnectionMethod.AccessKey:
|
||||||
|
return "Access Key";
|
||||||
|
case AwsConnectionMethod.AssumeRole:
|
||||||
|
return "Assume Role";
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||||
|
[AppConnection.AWS]: "AWS",
|
||||||
|
[AppConnection.GitHub]: "GitHub"
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
|
||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
|
export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||||
|
encryptedCredentials: true,
|
||||||
|
app: true,
|
||||||
|
method: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.CREATE(app).description)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.UPDATE(app).description)
|
||||||
|
});
|
360
backend/src/services/app-connection/app-connection-service.ts
Normal file
360
backend/src/services/app-connection/app-connection-service.ts
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
decryptAppConnectionCredentials,
|
||||||
|
encryptAppConnectionCredentials,
|
||||||
|
getAppConnectionMethodName,
|
||||||
|
listAppConnectionOptions,
|
||||||
|
validateAppConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import {
|
||||||
|
TAppConnection,
|
||||||
|
TAppConnectionConfig,
|
||||||
|
TCreateAppConnectionDTO,
|
||||||
|
TUpdateAppConnectionDTO,
|
||||||
|
TValidateAppConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
|
||||||
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // TODO: remove once launched
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||||
|
|
||||||
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||||
|
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||||
|
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appConnectionServiceFactory = ({
|
||||||
|
appConnectionDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
licenseService
|
||||||
|
}: TAppConnectionServiceFactoryDep) => {
|
||||||
|
// app connections are disabled for public until launch
|
||||||
|
const checkAppServicesAvailability = async (orgId: string) => {
|
||||||
|
const subscription = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
|
if (!subscription.appConnections) throw new BadRequestError({ message: "App Connections are not available yet." });
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const appConnections = await appConnectionDAL.find(
|
||||||
|
app
|
||||||
|
? { orgId: actor.orgId, app }
|
||||||
|
: {
|
||||||
|
orgId: actor.orgId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
appConnections
|
||||||
|
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||||
|
.map(async ({ encryptedCredentials, ...connection }) => {
|
||||||
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService,
|
||||||
|
orgId: connection.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
credentials
|
||||||
|
} as TAppConnection;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAppConnectionById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId });
|
||||||
|
|
||||||
|
if (!appConnection)
|
||||||
|
throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...appConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAppConnection = async (
|
||||||
|
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await appConnectionDAL.findOne(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
orgId: actor.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
credentials,
|
||||||
|
method,
|
||||||
|
orgId: actor.orgId
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = await appConnectionDAL.create(
|
||||||
|
{
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
method,
|
||||||
|
app,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
credentials: validatedCredentials
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return appConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAppConnection = async (
|
||||||
|
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
|
if (params.name && appConnection.name !== params.name) {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await appConnectionDAL.findOne(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
orgId: appConnection.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let encryptedCredentials: undefined | Buffer;
|
||||||
|
|
||||||
|
if (credentials) {
|
||||||
|
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||||
|
method,
|
||||||
|
credentials
|
||||||
|
}).success
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid credential format for ${
|
||||||
|
APP_CONNECTION_NAME_MAP[app]
|
||||||
|
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
credentials,
|
||||||
|
method
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
if (!validatedCredentials)
|
||||||
|
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||||
|
|
||||||
|
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConnection = await appConnectionDAL.updateById(
|
||||||
|
connectionId,
|
||||||
|
{
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedConnection;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedAppConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: updatedAppConnection.encryptedCredentials,
|
||||||
|
orgId: updatedAppConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
appConnection.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
|
if (appConnection.app !== app)
|
||||||
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
|
// TODO: specify delete error message if due to existing dependencies
|
||||||
|
|
||||||
|
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...deletedAppConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: deletedAppConnection.encryptedCredentials,
|
||||||
|
orgId: deletedAppConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listAppConnectionOptions,
|
||||||
|
listAppConnectionsByOrg,
|
||||||
|
findAppConnectionById,
|
||||||
|
findAppConnectionByName,
|
||||||
|
createAppConnection,
|
||||||
|
updateAppConnection,
|
||||||
|
deleteAppConnection
|
||||||
|
};
|
||||||
|
};
|
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
TAwsConnection,
|
||||||
|
TAwsConnectionConfig,
|
||||||
|
TAwsConnectionInput,
|
||||||
|
TValidateAwsConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
TGitHubConnection,
|
||||||
|
TGitHubConnectionConfig,
|
||||||
|
TGitHubConnectionInput,
|
||||||
|
TValidateGitHubConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
|
||||||
|
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
|
||||||
|
|
||||||
|
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
|
||||||
|
|
||||||
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
|
TAppConnectionInput,
|
||||||
|
"credentials" | "method" | "name" | "app" | "description"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
|
||||||
|
|
||||||
|
export type TValidateAppConnectionCredentials =
|
||||||
|
| TValidateAwsConnectionCredentials
|
||||||
|
| TValidateGitHubConnectionCredentials;
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum AwsConnectionMethod {
|
||||||
|
AssumeRole = "assume-role",
|
||||||
|
AccessKey = "access-key"
|
||||||
|
}
|
105
backend/src/services/app-connection/aws/aws-connection-fns.ts
Normal file
105
backend/src/services/app-connection/aws/aws-connection-fns.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||||
|
import AWS from "aws-sdk";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||||
|
import { TAwsConnectionConfig } from "./aws-connection-types";
|
||||||
|
|
||||||
|
export const getAwsAppConnectionListItem = () => {
|
||||||
|
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "AWS" as const,
|
||||||
|
app: AppConnection.AWS as const,
|
||||||
|
methods: Object.values(AwsConnectionMethod) as [AwsConnectionMethod.AssumeRole, AwsConnectionMethod.AccessKey],
|
||||||
|
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig, region = "us-east-1") => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
let accessKeyId: string;
|
||||||
|
let secretAccessKey: string;
|
||||||
|
let sessionToken: undefined | string;
|
||||||
|
|
||||||
|
const { method, credentials, orgId } = appConnection;
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case AwsConnectionMethod.AssumeRole: {
|
||||||
|
const client = new STSClient({
|
||||||
|
region,
|
||||||
|
credentials:
|
||||||
|
appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID && appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
|
||||||
|
? {
|
||||||
|
accessKeyId: appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
|
||||||
|
}
|
||||||
|
: undefined // if hosting on AWS
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new AssumeRoleCommand({
|
||||||
|
RoleArn: credentials.roleArn,
|
||||||
|
RoleSessionName: `infisical-app-connection-${randomUUID()}`,
|
||||||
|
DurationSeconds: 900, // 15 mins
|
||||||
|
ExternalId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const assumeRes = await client.send(command);
|
||||||
|
|
||||||
|
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
|
||||||
|
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
|
||||||
|
}
|
||||||
|
|
||||||
|
accessKeyId = assumeRes.Credentials.AccessKeyId;
|
||||||
|
secretAccessKey = assumeRes.Credentials.SecretAccessKey;
|
||||||
|
sessionToken = assumeRes.Credentials?.SessionToken;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AwsConnectionMethod.AccessKey: {
|
||||||
|
accessKeyId = credentials.accessKeyId;
|
||||||
|
secretAccessKey = credentials.secretAccessKey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new InternalServerError({ message: `Unsupported AWS connection method: ${method}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AWS.Config({
|
||||||
|
region,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId,
|
||||||
|
secretAccessKey,
|
||||||
|
sessionToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
|
||||||
|
const awsConfig = await getAwsConnectionConfig(appConnection);
|
||||||
|
const sts = new AWS.STS(awsConfig);
|
||||||
|
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
resp = await sts.getCallerIdentity().promise();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection - verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.$response.httpResponse.statusCode !== 200)
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Unable to validate credentials: ${
|
||||||
|
resp.$response.error?.message ??
|
||||||
|
`AWS responded with a status code of ${resp.$response.httpResponse.statusCode}. Verify credentials and try again.`
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
|
||||||
|
return appConnection.credentials;
|
||||||
|
};
|
@ -0,0 +1,82 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||||
|
|
||||||
|
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
|
||||||
|
roleArn: z.string().trim().min(1, "Role ARN required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsConnectionAccessTokenCredentialsSchema = z.object({
|
||||||
|
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
|
||||||
|
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseAwsConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AWS) });
|
||||||
|
|
||||||
|
export const AwsConnectionSchema = z.intersection(
|
||||||
|
BaseAwsConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||||
|
credentials: AwsConnectionAssumeRoleCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||||
|
credentials: AwsConnectionAccessTokenCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseAwsConnectionSchema.extend({
|
||||||
|
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||||
|
credentials: AwsConnectionAssumeRoleCredentialsSchema.omit({ roleArn: true })
|
||||||
|
}),
|
||||||
|
BaseAwsConnectionSchema.extend({
|
||||||
|
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||||
|
credentials: AwsConnectionAccessTokenCredentialsSchema.omit({ secretAccessKey: true })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||||
|
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||||
|
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AWS).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateAwsConnectionSchema = ValidateAwsConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.AWS)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateAwsConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: z
|
||||||
|
.union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.AWS).credentials)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS));
|
||||||
|
|
||||||
|
export const AwsConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("AWS"),
|
||||||
|
app: z.literal(AppConnection.AWS),
|
||||||
|
// the below is preferable but currently breaks mintlify
|
||||||
|
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||||
|
methods: z.nativeEnum(AwsConnectionMethod).array(),
|
||||||
|
accessKeyId: z.string().optional()
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AwsConnectionSchema,
|
||||||
|
CreateAwsConnectionSchema,
|
||||||
|
ValidateAwsConnectionCredentialsSchema
|
||||||
|
} from "./aws-connection-schemas";
|
||||||
|
|
||||||
|
export type TAwsConnection = z.infer<typeof AwsConnectionSchema>;
|
||||||
|
|
||||||
|
export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
|
||||||
|
app: AppConnection.AWS;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
4
backend/src/services/app-connection/aws/index.ts
Normal file
4
backend/src/services/app-connection/aws/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./aws-connection-enums";
|
||||||
|
export * from "./aws-connection-fns";
|
||||||
|
export * from "./aws-connection-schemas";
|
||||||
|
export * from "./aws-connection-types";
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum GitHubConnectionMethod {
|
||||||
|
OAuth = "oauth",
|
||||||
|
App = "github-app"
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||||
|
import { TGitHubConnectionConfig } from "./github-connection-types";
|
||||||
|
|
||||||
|
export const getGitHubConnectionListItem = () => {
|
||||||
|
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "GitHub" as const,
|
||||||
|
app: AppConnection.GitHub as const,
|
||||||
|
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
|
||||||
|
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
|
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TokenRespData = {
|
||||||
|
access_token: string;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
||||||
|
const { credentials, method } = config;
|
||||||
|
|
||||||
|
const {
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||||
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET,
|
||||||
|
SITE_URL
|
||||||
|
} = getConfig();
|
||||||
|
|
||||||
|
const { clientId, clientSecret } =
|
||||||
|
method === GitHubConnectionMethod.App
|
||||||
|
? {
|
||||||
|
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
|
||||||
|
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
|
||||||
|
}
|
||||||
|
: // oauth
|
||||||
|
{
|
||||||
|
clientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
|
clientSecret: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!clientId || !clientSecret) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `GitHub ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenResp: AxiosResponse<TokenRespData>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
|
||||||
|
params: {
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
code: credentials.code,
|
||||||
|
redirect_uri: `${SITE_URL}/app-connections/github/oauth/callback`
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e: unknown) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection - verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenResp.status !== 200) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === GitHubConnectionMethod.App) {
|
||||||
|
const installationsResp = await request.get<{
|
||||||
|
installations: {
|
||||||
|
id: number;
|
||||||
|
account: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${tokenResp.data.access_token}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchingInstallation = installationsResp.data.installations.find(
|
||||||
|
(installation) => installation.id === +credentials.installationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingInstallation) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "User does not have access to the provided installation"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
return {
|
||||||
|
// access token not needed for GitHub App
|
||||||
|
installationId: credentials.installationId
|
||||||
|
};
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
return {
|
||||||
|
accessToken: tokenResp.data.access_token
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,93 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||||
|
|
||||||
|
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
|
||||||
|
code: z.string().trim().min(1, "OAuth code required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GitHubConnectionAppInputCredentialsSchema = z.object({
|
||||||
|
code: z.string().trim().min(1, "GitHub App code required"),
|
||||||
|
installationId: z.string().min(1, "GitHub App Installation ID required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
|
||||||
|
accessToken: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
|
||||||
|
installationId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||||
|
credentials: GitHubConnectionAppInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||||
|
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateGitHubConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: z
|
||||||
|
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub));
|
||||||
|
|
||||||
|
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
||||||
|
|
||||||
|
export const GitHubAppConnectionSchema = z.intersection(
|
||||||
|
BaseGitHubConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.App),
|
||||||
|
credentials: GitHubConnectionAppOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||||
|
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseGitHubConnectionSchema.extend({
|
||||||
|
method: z.literal(GitHubConnectionMethod.App),
|
||||||
|
credentials: GitHubConnectionAppOutputCredentialsSchema.omit({ installationId: true })
|
||||||
|
}),
|
||||||
|
BaseGitHubConnectionSchema.extend({
|
||||||
|
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||||
|
credentials: GitHubConnectionOAuthOutputCredentialsSchema.omit({ accessToken: true })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const GitHubConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("GitHub"),
|
||||||
|
app: z.literal(AppConnection.GitHub),
|
||||||
|
// the below is preferable but currently breaks mintlify
|
||||||
|
// methods: z.tuple([z.literal(GitHubConnectionMethod.GitHubApp), z.literal(GitHubConnectionMethod.OAuth)]),
|
||||||
|
methods: z.nativeEnum(GitHubConnectionMethod).array(),
|
||||||
|
oauthClientId: z.string().optional(),
|
||||||
|
appClientSlug: z.string().optional()
|
||||||
|
});
|
@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateGitHubConnectionSchema,
|
||||||
|
GitHubAppConnectionSchema,
|
||||||
|
ValidateGitHubConnectionCredentialsSchema
|
||||||
|
} from "./github-connection-schemas";
|
||||||
|
|
||||||
|
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
|
||||||
|
|
||||||
|
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
|
||||||
|
app: AppConnection.GitHub;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
|
4
backend/src/services/app-connection/github/index.ts
Normal file
4
backend/src/services/app-connection/github/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./github-connection-enums";
|
||||||
|
export * from "./github-connection-fns";
|
||||||
|
export * from "./github-connection-schemas";
|
||||||
|
export * from "./github-connection-types";
|
@ -12,9 +12,12 @@ export type TTokenDALFactory = ReturnType<typeof tokenDALFactory>;
|
|||||||
export const tokenDALFactory = (db: TDbClient) => {
|
export const tokenDALFactory = (db: TDbClient) => {
|
||||||
const authOrm = ormify(db, TableName.AuthTokens);
|
const authOrm = ormify(db, TableName.AuthTokens);
|
||||||
|
|
||||||
const findOneTokenSession = async (filter: Partial<TAuthTokenSessions>): Promise<TAuthTokenSessions | undefined> => {
|
const findOneTokenSession = async (
|
||||||
|
filter: Partial<TAuthTokenSessions>,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TAuthTokenSessions | undefined> => {
|
||||||
try {
|
try {
|
||||||
const doc = await db.replicaNode()(TableName.AuthTokenSession).where(filter).first();
|
const doc = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter).first();
|
||||||
return doc;
|
return doc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindOneTokenSession" });
|
throw new DatabaseError({ error, name: "FindOneTokenSession" });
|
||||||
@ -54,10 +57,11 @@ export const tokenDALFactory = (db: TDbClient) => {
|
|||||||
const insertTokenSession = async (
|
const insertTokenSession = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
ip: string,
|
ip: string,
|
||||||
userAgent: string
|
userAgent: string,
|
||||||
|
tx?: Knex
|
||||||
): Promise<TAuthTokenSessions | undefined> => {
|
): Promise<TAuthTokenSessions | undefined> => {
|
||||||
try {
|
try {
|
||||||
const [session] = await db(TableName.AuthTokenSession)
|
const [session] = await (tx || db)(TableName.AuthTokenSession)
|
||||||
.insert({
|
.insert({
|
||||||
userId,
|
userId,
|
||||||
ip,
|
ip,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@ -123,14 +124,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
return deletedToken?.[0];
|
return deletedToken?.[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserTokenSession = async ({
|
const getUserTokenSession = async (
|
||||||
userId,
|
{ userId, ip, userAgent }: TIssueAuthTokenDTO,
|
||||||
ip,
|
tx?: Knex
|
||||||
userAgent
|
): Promise<TAuthTokenSessions | undefined> => {
|
||||||
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
|
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent }, tx);
|
||||||
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent });
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
session = await tokenDAL.insertTokenSession(userId, ip, userAgent);
|
session = await tokenDAL.insertTokenSession(userId, ip, userAgent, tx);
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
@ -50,13 +51,13 @@ export const authLoginServiceFactory = ({
|
|||||||
* Not exported. This is to update user device list
|
* Not exported. This is to update user device list
|
||||||
* If new device is found. Will be saved and a mail will be send
|
* If new device is found. Will be saved and a mail will be send
|
||||||
*/
|
*/
|
||||||
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
|
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string, tx?: Knex) => {
|
||||||
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
|
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
|
||||||
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
|
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
|
||||||
|
|
||||||
if (!isDeviceSeen) {
|
if (!isDeviceSeen) {
|
||||||
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||||
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
|
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
|
||||||
if (user.email) {
|
if (user.email) {
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.NewDeviceJoin,
|
template: SmtpTemplates.NewDeviceJoin,
|
||||||
@ -97,30 +98,36 @@ export const authLoginServiceFactory = ({
|
|||||||
* Check user device and send mail if new device
|
* Check user device and send mail if new device
|
||||||
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
|
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
|
||||||
*/
|
*/
|
||||||
const generateUserTokens = async ({
|
const generateUserTokens = async (
|
||||||
user,
|
{
|
||||||
ip,
|
user,
|
||||||
userAgent,
|
|
||||||
organizationId,
|
|
||||||
authMethod,
|
|
||||||
isMfaVerified,
|
|
||||||
mfaMethod
|
|
||||||
}: {
|
|
||||||
user: TUsers;
|
|
||||||
ip: string;
|
|
||||||
userAgent: string;
|
|
||||||
organizationId?: string;
|
|
||||||
authMethod: AuthMethod;
|
|
||||||
isMfaVerified?: boolean;
|
|
||||||
mfaMethod?: MfaMethod;
|
|
||||||
}) => {
|
|
||||||
const cfg = getConfig();
|
|
||||||
await updateUserDeviceSession(user, ip, userAgent);
|
|
||||||
const tokenSession = await tokenService.getUserTokenSession({
|
|
||||||
userAgent,
|
|
||||||
ip,
|
ip,
|
||||||
userId: user.id
|
userAgent,
|
||||||
});
|
organizationId,
|
||||||
|
authMethod,
|
||||||
|
isMfaVerified,
|
||||||
|
mfaMethod
|
||||||
|
}: {
|
||||||
|
user: TUsers;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
organizationId?: string;
|
||||||
|
authMethod: AuthMethod;
|
||||||
|
isMfaVerified?: boolean;
|
||||||
|
mfaMethod?: MfaMethod;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
await updateUserDeviceSession(user, ip, userAgent, tx);
|
||||||
|
const tokenSession = await tokenService.getUserTokenSession(
|
||||||
|
{
|
||||||
|
userAgent,
|
||||||
|
ip,
|
||||||
|
userId: user.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
|
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
export const createSerialNumber = () => {
|
export const createSerialNumber = () => {
|
||||||
const randomBytes = crypto.randomBytes(20);
|
const randomBytes = crypto.randomBytes(20); // 20 bytes = 160 bits
|
||||||
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
||||||
return randomBytes.toString("hex");
|
return randomBytes.toString("hex");
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
export type TCircleCIContext = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
@ -17,6 +17,8 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
@ -24,6 +26,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { getApps } from "./integration-app-list";
|
import { getApps } from "./integration-app-list";
|
||||||
|
import { TCircleCIContext } from "./integration-app-types";
|
||||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||||
import {
|
import {
|
||||||
@ -31,6 +34,7 @@ import {
|
|||||||
TBitbucketEnvironment,
|
TBitbucketEnvironment,
|
||||||
TBitbucketWorkspace,
|
TBitbucketWorkspace,
|
||||||
TChecklyGroups,
|
TChecklyGroups,
|
||||||
|
TCircleCIOrganization,
|
||||||
TDeleteIntegrationAuthByIdDTO,
|
TDeleteIntegrationAuthByIdDTO,
|
||||||
TDeleteIntegrationAuthsDTO,
|
TDeleteIntegrationAuthsDTO,
|
||||||
TDuplicateGithubIntegrationAuthDTO,
|
TDuplicateGithubIntegrationAuthDTO,
|
||||||
@ -42,6 +46,7 @@ import {
|
|||||||
TIntegrationAuthBitbucketEnvironmentsDTO,
|
TIntegrationAuthBitbucketEnvironmentsDTO,
|
||||||
TIntegrationAuthBitbucketWorkspaceDTO,
|
TIntegrationAuthBitbucketWorkspaceDTO,
|
||||||
TIntegrationAuthChecklyGroupsDTO,
|
TIntegrationAuthChecklyGroupsDTO,
|
||||||
|
TIntegrationAuthCircleCIOrganizationDTO,
|
||||||
TIntegrationAuthGithubEnvsDTO,
|
TIntegrationAuthGithubEnvsDTO,
|
||||||
TIntegrationAuthGithubOrgsDTO,
|
TIntegrationAuthGithubOrgsDTO,
|
||||||
TIntegrationAuthHerokuPipelinesDTO,
|
TIntegrationAuthHerokuPipelinesDTO,
|
||||||
@ -1578,6 +1583,120 @@ export const integrationAuthServiceFactory = ({
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCircleCIOrganizations = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TIntegrationAuthCircleCIOrganizationDTO) => {
|
||||||
|
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||||
|
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integrationAuth.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||||
|
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||||
|
|
||||||
|
const { data: organizations }: { data: TCircleCIOrganization[] } = await request.get(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": `${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let projects: {
|
||||||
|
orgName: string;
|
||||||
|
projectName: string;
|
||||||
|
projectId?: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectRes = (
|
||||||
|
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
projects = projectRes.map((a) => ({
|
||||||
|
orgName: a.username, // username maps to unique organization name in CircleCI
|
||||||
|
projectName: a.reponame, // reponame maps to project name within an organization in CircleCI
|
||||||
|
projectId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectsByOrg = groupBy(
|
||||||
|
projects.map((p) => ({
|
||||||
|
orgName: p.orgName,
|
||||||
|
name: p.projectName,
|
||||||
|
id: p.projectId as string
|
||||||
|
})),
|
||||||
|
(p) => p.orgName
|
||||||
|
);
|
||||||
|
|
||||||
|
const getOrgContexts = async (orgSlug: string) => {
|
||||||
|
type NextPageToken = string | null | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contexts: TCircleCIContext[] = [];
|
||||||
|
let nextPageToken: NextPageToken;
|
||||||
|
|
||||||
|
while (nextPageToken !== null) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const { data } = await request.get<{
|
||||||
|
items: TCircleCIContext[];
|
||||||
|
next_page_token: NextPageToken;
|
||||||
|
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context`, {
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
},
|
||||||
|
params: new URLSearchParams({
|
||||||
|
"owner-slug": orgSlug,
|
||||||
|
...(nextPageToken ? { "page-token": nextPageToken } : {})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
contexts.push(...data.items);
|
||||||
|
nextPageToken = data.next_page_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contexts?.map((context) => ({
|
||||||
|
name: context.name,
|
||||||
|
id: context.id
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
organizations.map(async (org) => ({
|
||||||
|
name: org.name,
|
||||||
|
slug: org.slug,
|
||||||
|
projects: projectsByOrg[org.name] ?? [],
|
||||||
|
contexts: (await getOrgContexts(org.slug)) ?? []
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const deleteIntegrationAuths = async ({
|
const deleteIntegrationAuths = async ({
|
||||||
projectId,
|
projectId,
|
||||||
integration,
|
integration,
|
||||||
@ -1790,6 +1909,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
getTeamcityBuildConfigs,
|
getTeamcityBuildConfigs,
|
||||||
getBitbucketWorkspaces,
|
getBitbucketWorkspaces,
|
||||||
getBitbucketEnvironments,
|
getBitbucketEnvironments,
|
||||||
|
getCircleCIOrganizations,
|
||||||
getIntegrationAccessToken,
|
getIntegrationAccessToken,
|
||||||
duplicateIntegrationAuth,
|
duplicateIntegrationAuth,
|
||||||
getOctopusDeploySpaces,
|
getOctopusDeploySpaces,
|
||||||
|
@ -128,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
|
|||||||
appId: string;
|
appId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthCircleCIOrganizationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TVercelBranches = {
|
export type TVercelBranches = {
|
||||||
ref: string;
|
ref: string;
|
||||||
lastCommit: string;
|
lastCommit: string;
|
||||||
@ -189,6 +193,14 @@ export type TTeamCityBuildConfig = {
|
|||||||
webUrl: string;
|
webUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCircleCIOrganization = {
|
||||||
|
id: string;
|
||||||
|
vcsType: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TIntegrationsWithEnvironment = TIntegrations & {
|
export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||||
environment?:
|
environment?:
|
||||||
| {
|
| {
|
||||||
@ -215,6 +227,11 @@ export enum OctopusDeployScope {
|
|||||||
// add tenant, variable set, etc.
|
// add tenant, variable set, etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CircleCiScope {
|
||||||
|
Project = "project",
|
||||||
|
Context = "context"
|
||||||
|
}
|
||||||
|
|
||||||
export type TOctopusDeployVariableSet = {
|
export type TOctopusDeployVariableSet = {
|
||||||
Id: string;
|
Id: string;
|
||||||
OwnerId: string;
|
OwnerId: string;
|
||||||
|
@ -76,7 +76,6 @@ export enum IntegrationUrls {
|
|||||||
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
||||||
FLYIO_API_URL = "https://api.fly.io/graphql",
|
FLYIO_API_URL = "https://api.fly.io/graphql",
|
||||||
CIRCLECI_API_URL = "https://circleci.com/api",
|
CIRCLECI_API_URL = "https://circleci.com/api",
|
||||||
DATABRICKS_API_URL = "https:/xxxx.com/api",
|
|
||||||
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
||||||
SUPABASE_API_URL = "https://api.supabase.com",
|
SUPABASE_API_URL = "https://api.supabase.com",
|
||||||
LARAVELFORGE_API_URL = "https://forge.laravel.com",
|
LARAVELFORGE_API_URL = "https://forge.laravel.com",
|
||||||
@ -218,9 +217,9 @@ export const getIntegrationOptions = async () => {
|
|||||||
docsLink: ""
|
docsLink: ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Circle CI",
|
name: "CircleCI",
|
||||||
slug: "circleci",
|
slug: "circleci",
|
||||||
image: "Circle CI.png",
|
image: "CircleCI.png",
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
type: "pat",
|
type: "pat",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
export const isAzureKeyVaultReference = (uri: string) => {
|
||||||
|
const tryJsonDecode = () => {
|
||||||
|
try {
|
||||||
|
return (JSON.parse(uri) as { uri: string }).uri || uri;
|
||||||
|
} catch {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanUri = tryJsonDecode();
|
||||||
|
|
||||||
|
if (!cleanUri.startsWith("https://")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cleanUri.includes(".vault.azure.net/secrets/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check for non-empty string between https:// and .vault.azure.net/secrets/
|
||||||
|
const parts = cleanUri.split(".vault.azure.net/secrets/");
|
||||||
|
const vaultName = parts[0].replace("https://", "");
|
||||||
|
if (!vaultName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check for non-empty secret name
|
||||||
|
const secretParts = parts[1].split("/");
|
||||||
|
const secretName = secretParts[0];
|
||||||
|
if (!secretName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
@ -39,13 +39,19 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
|
|||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||||
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
|
||||||
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types";
|
import {
|
||||||
|
CircleCiScope,
|
||||||
|
OctopusDeployScope,
|
||||||
|
TIntegrationsWithEnvironment,
|
||||||
|
TOctopusDeployVariableSet
|
||||||
|
} from "./integration-auth-types";
|
||||||
import {
|
import {
|
||||||
IntegrationInitialSyncBehavior,
|
IntegrationInitialSyncBehavior,
|
||||||
IntegrationMappingBehavior,
|
IntegrationMappingBehavior,
|
||||||
Integrations,
|
Integrations,
|
||||||
IntegrationUrls
|
IntegrationUrls
|
||||||
} from "./integration-list";
|
} from "./integration-list";
|
||||||
|
import { isAzureKeyVaultReference } from "./integration-sync-secret-fns";
|
||||||
|
|
||||||
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
|
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
|
||||||
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
|
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
|
||||||
@ -320,11 +326,12 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
const azureAppConfigSecrets = (
|
|
||||||
await getCompleteAzureAppConfigValues(
|
const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
|
||||||
`${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*`
|
metadata.azureLabel ? `&label=${metadata.azureLabel}` : ""
|
||||||
)
|
}`;
|
||||||
).reduce(
|
|
||||||
|
const azureAppConfigSecrets = (await getCompleteAzureAppConfigValues(azureAppConfigValuesUrl)).reduce(
|
||||||
(accum, entry) => {
|
(accum, entry) => {
|
||||||
accum[entry.key] = entry.value;
|
accum[entry.key] = entry.value;
|
||||||
|
|
||||||
@ -405,14 +412,24 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create or update secrets on Azure App Config
|
// create or update secrets on Azure App Config
|
||||||
|
|
||||||
for await (const key of Object.keys(secrets)) {
|
for await (const key of Object.keys(secrets)) {
|
||||||
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
|
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
|
||||||
await request.put(
|
await request.put(
|
||||||
`${integration.app}/kv/${key}?api-version=2023-11-01`,
|
`${integration.app}/kv/${key}?api-version=2023-11-01`,
|
||||||
{
|
{
|
||||||
value: secrets[key]?.value
|
value: secrets[key]?.value,
|
||||||
|
...(isAzureKeyVaultReference(secrets[key]?.value || "") && {
|
||||||
|
content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
...(metadata.azureLabel && {
|
||||||
|
params: {
|
||||||
|
label: metadata.azureLabel
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
},
|
},
|
||||||
@ -432,6 +449,11 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
},
|
},
|
||||||
|
...(metadata.azureLabel && {
|
||||||
|
params: {
|
||||||
|
label: metadata.azureLabel
|
||||||
|
}
|
||||||
|
}),
|
||||||
// we force IPV4 because docker setup fails with ipv6
|
// we force IPV4 because docker setup fails with ipv6
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: new https.Agent({
|
||||||
family: 4
|
family: 4
|
||||||
@ -1375,14 +1397,24 @@ const syncSecretsHeroku = async ({
|
|||||||
* Sync/push [secrets] to Vercel project named [integration.app]
|
* Sync/push [secrets] to Vercel project named [integration.app]
|
||||||
*/
|
*/
|
||||||
const syncSecretsVercel = async ({
|
const syncSecretsVercel = async ({
|
||||||
|
createManySecretsRawFn,
|
||||||
integration,
|
integration,
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
secrets,
|
secrets: infisicalSecrets,
|
||||||
accessToken
|
accessToken
|
||||||
}: {
|
}: {
|
||||||
integration: TIntegrations;
|
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
|
||||||
|
integration: TIntegrations & {
|
||||||
|
projectId: string;
|
||||||
|
environment: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
integrationAuth: TIntegrationAuths;
|
integrationAuth: TIntegrationAuths;
|
||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string } | null>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
interface VercelSecret {
|
interface VercelSecret {
|
||||||
@ -1455,80 +1487,119 @@ const syncSecretsVercel = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSecrets: VercelSecret[] = [];
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
const deleteSecrets: VercelSecret[] = [];
|
|
||||||
const newSecrets: VercelSecret[] = [];
|
|
||||||
|
|
||||||
// Identify secrets to create
|
// Default to overwrite target for old integrations that doesn't have a initial sync behavior set.
|
||||||
Object.keys(secrets).forEach((key) => {
|
if (!metadata.initialSyncBehavior) {
|
||||||
if (!(key in res)) {
|
metadata.initialSyncBehavior = IntegrationInitialSyncBehavior.OVERWRITE_TARGET;
|
||||||
// case: secret has been created
|
}
|
||||||
newSecrets.push({
|
|
||||||
key,
|
const secretsToAddToInfisical: { [key: string]: VercelSecret } = {};
|
||||||
value: secrets[key].value,
|
|
||||||
type: "encrypted",
|
Object.keys(res).forEach((vercelKey) => {
|
||||||
target: [integration.targetEnvironment as string],
|
if (!integration.lastUsed) {
|
||||||
...(integration.path
|
// first time using integration
|
||||||
? {
|
// -> apply initial sync behavior
|
||||||
gitBranch: integration.path
|
switch (metadata.initialSyncBehavior) {
|
||||||
}
|
// Override all the secrets in Vercel
|
||||||
: {})
|
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
|
||||||
});
|
if (!(vercelKey in infisicalSecrets)) infisicalSecrets[vercelKey] = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
|
||||||
|
// if the vercel secret is not in infisical, we need to add it to infisical
|
||||||
|
if (!(vercelKey in infisicalSecrets)) {
|
||||||
|
infisicalSecrets[vercelKey] = {
|
||||||
|
value: res[vercelKey].value
|
||||||
|
};
|
||||||
|
secretsToAddToInfisical[vercelKey] = res[vercelKey];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Invalid initial sync behavior: ${metadata.initialSyncBehavior}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!(vercelKey in infisicalSecrets)) {
|
||||||
|
infisicalSecrets[vercelKey] = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Identify secrets to update and delete
|
if (Object.keys(secretsToAddToInfisical).length) {
|
||||||
Object.keys(res).forEach((key) => {
|
await createManySecretsRawFn({
|
||||||
if (key in secrets) {
|
projectId: integration.projectId,
|
||||||
if (res[key].value !== secrets[key].value) {
|
environment: integration.environment.slug,
|
||||||
// case: secret value has changed
|
path: integration.secretPath,
|
||||||
updateSecrets.push({
|
secrets: Object.keys(secretsToAddToInfisical).map((key) => ({
|
||||||
id: res[key].id,
|
secretName: key,
|
||||||
key,
|
secretValue: secretsToAddToInfisical[key].value,
|
||||||
value: secrets[key].value,
|
type: SecretType.Shared,
|
||||||
type: res[key].type,
|
secretComment: ""
|
||||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
}))
|
||||||
? [...res[key].target]
|
|
||||||
: [...res[key].target, integration.targetEnvironment as string],
|
|
||||||
...(integration.path
|
|
||||||
? {
|
|
||||||
gitBranch: integration.path
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// case: secret has been deleted
|
|
||||||
deleteSecrets.push({
|
|
||||||
id: res[key].id,
|
|
||||||
key,
|
|
||||||
value: res[key].value,
|
|
||||||
type: "encrypted", // value doesn't matter
|
|
||||||
target: [integration.targetEnvironment as string],
|
|
||||||
...(integration.path
|
|
||||||
? {
|
|
||||||
gitBranch: integration.path
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync/push new secrets
|
|
||||||
if (newSecrets.length > 0) {
|
|
||||||
await request.post(`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, {
|
|
||||||
params,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const secret of updateSecrets) {
|
// update and create logic
|
||||||
if (secret.type !== "sensitive") {
|
for await (const key of Object.keys(infisicalSecrets)) {
|
||||||
const { id, ...updatedSecret } = secret;
|
if (!(key in res) || infisicalSecrets[key]?.value !== res[key].value) {
|
||||||
await request.patch(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${id}`, updatedSecret, {
|
// if the key is not in the vercel res, we need to create it
|
||||||
|
if (!(key in res)) {
|
||||||
|
await request.post(
|
||||||
|
`${IntegrationUrls.VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
value: infisicalSecrets[key]?.value,
|
||||||
|
type: "encrypted",
|
||||||
|
target: [integration.targetEnvironment as string],
|
||||||
|
...(integration.path
|
||||||
|
? {
|
||||||
|
gitBranch: integration.path
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Else if the key already exists and its not sensitive, we need to update it
|
||||||
|
} else if (res[key].type !== "sensitive") {
|
||||||
|
await request.patch(
|
||||||
|
`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`,
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
value: infisicalSecrets[key]?.value,
|
||||||
|
type: res[key].type,
|
||||||
|
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||||
|
? [...res[key].target]
|
||||||
|
: [...res[key].target, integration.targetEnvironment as string],
|
||||||
|
...(integration.path
|
||||||
|
? {
|
||||||
|
gitBranch: integration.path
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete logic
|
||||||
|
for await (const key of Object.keys(res)) {
|
||||||
|
if (infisicalSecrets[key] === null) {
|
||||||
|
// case: delete secret
|
||||||
|
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${res[key].id}`, {
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
@ -1537,16 +1608,6 @@ const syncSecretsVercel = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const secret of deleteSecrets) {
|
|
||||||
await request.delete(`${IntegrationUrls.VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, {
|
|
||||||
params,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Accept-Encoding": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2245,102 +2306,174 @@ const syncSecretsCircleCI = async ({
|
|||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
const getProjectSlug = async () => {
|
if (integration.scope === CircleCiScope.Context) {
|
||||||
const requestConfig = {
|
// sync secrets to CircleCI
|
||||||
headers: {
|
await Promise.all(
|
||||||
"Circle-Token": accessToken,
|
Object.keys(secrets).map(async (key) =>
|
||||||
"Accept-Encoding": "application/json"
|
request.put(
|
||||||
}
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`,
|
||||||
};
|
{
|
||||||
|
value: secrets[key].value
|
||||||
try {
|
},
|
||||||
const projectDetails = (
|
{
|
||||||
await request.get<{ slug: string }>(
|
headers: {
|
||||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
|
"Circle-Token": accessToken,
|
||||||
requestConfig
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
).data;
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return projectDetails.slug;
|
// get secrets from CircleCI
|
||||||
} catch (err) {
|
const getSecretsRes = async () => {
|
||||||
if (err instanceof AxiosError) {
|
type EnvVars = {
|
||||||
if (err.response?.data?.message !== "Not Found") {
|
variable: string;
|
||||||
throw new Error("Failed to get project slug from CircleCI during first attempt.");
|
created_at: string;
|
||||||
}
|
updated_at: string;
|
||||||
}
|
context_id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
|
let nextPageToken: string | null | undefined;
|
||||||
try {
|
const envVars: EnvVars[] = [];
|
||||||
const circleCiOrganization = (
|
|
||||||
await request.get<{ slug: string; name: string }[]>(
|
|
||||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
|
||||||
requestConfig
|
|
||||||
)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
|
while (nextPageToken !== null) {
|
||||||
if (integration.owner) {
|
const res = await request.get<{
|
||||||
const org = circleCiOrganization.find((o) => o.name === integration.owner);
|
items: EnvVars[];
|
||||||
if (org) {
|
next_page_token: string | null;
|
||||||
return `${org.slug}/${integration.app}`;
|
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
|
|
||||||
return `${circleCiOrganization[0].slug}/${integration.app}`;
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error("Failed to get project slug from CircleCI during second attempt.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectSlug = await getProjectSlug();
|
|
||||||
|
|
||||||
// sync secrets to CircleCI
|
|
||||||
await Promise.all(
|
|
||||||
Object.keys(secrets).map(async (key) =>
|
|
||||||
request.post(
|
|
||||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
|
||||||
{
|
|
||||||
name: key,
|
|
||||||
value: secrets[key].value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
"Circle-Token": accessToken,
|
"Circle-Token": accessToken,
|
||||||
"Content-Type": "application/json"
|
"Accept-Encoding": "application/json"
|
||||||
}
|
},
|
||||||
}
|
params: nextPageToken
|
||||||
)
|
? new URLSearchParams({
|
||||||
)
|
"page-token": nextPageToken
|
||||||
);
|
})
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
|
||||||
// get secrets from CircleCI
|
envVars.push(...res.data.items);
|
||||||
const getSecretsRes = (
|
nextPageToken = res.data.next_page_token;
|
||||||
await request.get<{ items: { name: string }[] }>(
|
}
|
||||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
|
||||||
{
|
return envVars;
|
||||||
|
};
|
||||||
|
|
||||||
|
// delete secrets from CircleCI
|
||||||
|
await Promise.all(
|
||||||
|
(await getSecretsRes()).map(async (sec) => {
|
||||||
|
if (!(sec.variable in secrets)) {
|
||||||
|
return request.delete(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const getProjectSlug = async () => {
|
||||||
|
const requestConfig = {
|
||||||
headers: {
|
headers: {
|
||||||
"Circle-Token": accessToken,
|
"Circle-Token": accessToken,
|
||||||
"Accept-Encoding": "application/json"
|
"Accept-Encoding": "application/json"
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
)
|
|
||||||
).data?.items;
|
|
||||||
|
|
||||||
// delete secrets from CircleCI
|
try {
|
||||||
await Promise.all(
|
const projectDetails = (
|
||||||
getSecretsRes.map(async (sec) => {
|
await request.get<{ slug: string }>(
|
||||||
if (!(sec.name in secrets)) {
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
|
||||||
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
|
requestConfig
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return projectDetails.slug;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response?.data?.message !== "Not Found") {
|
||||||
|
throw new Error("Failed to get project slug from CircleCI during first attempt.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
|
||||||
|
try {
|
||||||
|
const circleCiOrganization = (
|
||||||
|
await request.get<{ slug: string; name: string }[]>(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||||
|
requestConfig
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
|
||||||
|
if (integration.owner) {
|
||||||
|
const org = circleCiOrganization.find((o) => o.name === integration.owner);
|
||||||
|
if (org) {
|
||||||
|
return `${org.slug}/${integration.app}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
|
||||||
|
return `${circleCiOrganization[0].slug}/${integration.app}`;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Failed to get project slug from CircleCI during second attempt.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectSlug = await getProjectSlug();
|
||||||
|
|
||||||
|
// sync secrets to CircleCI
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(secrets).map(async (key) =>
|
||||||
|
request.post(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||||
|
{
|
||||||
|
name: key,
|
||||||
|
value: secrets[key].value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// get secrets from CircleCI
|
||||||
|
const getSecretsRes = (
|
||||||
|
await request.get<{ items: { name: string }[] }>(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Circle-Token": accessToken,
|
"Circle-Token": accessToken,
|
||||||
"Content-Type": "application/json"
|
"Accept-Encoding": "application/json"
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
)
|
||||||
})
|
).data?.items;
|
||||||
);
|
|
||||||
|
// delete secrets from CircleCI
|
||||||
|
await Promise.all(
|
||||||
|
getSecretsRes.map(async (sec) => {
|
||||||
|
if (!(sec.name in secrets)) {
|
||||||
|
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4377,7 +4510,8 @@ export const syncIntegrationSecrets = async ({
|
|||||||
integration,
|
integration,
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken,
|
||||||
|
createManySecretsRawFn
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Integrations.NETLIFY:
|
case Integrations.NETLIFY:
|
||||||
|
@ -35,6 +35,8 @@ export const IntegrationMetadataSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||||
|
|
||||||
|
azureLabel: z.string().optional().describe(INTEGRATION.CREATE.metadata.azureLabel),
|
||||||
|
|
||||||
githubVisibility: z
|
githubVisibility: z
|
||||||
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
|
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
|
||||||
.optional()
|
.optional()
|
||||||
|
16
backend/src/services/org/org-schema.ts
Normal file
16
backend/src/services/org/org-schema.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { OrganizationsSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
customerId: true,
|
||||||
|
slug: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
authEnforced: true,
|
||||||
|
scimEnabled: true,
|
||||||
|
kmsDefaultKeyId: true,
|
||||||
|
defaultMembershipRole: true,
|
||||||
|
enforceMfa: true,
|
||||||
|
selectedMfaMethod: true
|
||||||
|
});
|
@ -31,11 +31,13 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
|
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||||
|
import { ActorAuthMethod, ActorType, AuthMethod, AuthModeJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
||||||
@ -47,6 +49,10 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||||
@ -69,6 +75,9 @@ import {
|
|||||||
|
|
||||||
type TOrgServiceFactoryDep = {
|
type TOrgServiceFactoryDep = {
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
orgBotDAL: TOrgBotDALFactory;
|
orgBotDAL: TOrgBotDALFactory;
|
||||||
orgRoleDAL: TOrgRoleDALFactory;
|
orgRoleDAL: TOrgRoleDALFactory;
|
||||||
@ -97,6 +106,8 @@ type TOrgServiceFactoryDep = {
|
|||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
|
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||||
@ -104,6 +115,9 @@ export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
|||||||
export const orgServiceFactory = ({
|
export const orgServiceFactory = ({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
folderDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@ -124,7 +138,9 @@ export const orgServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
projectBotService
|
projectBotService,
|
||||||
|
queueService,
|
||||||
|
loginService
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
@ -419,24 +435,88 @@ export const orgServiceFactory = ({
|
|||||||
/*
|
/*
|
||||||
* Delete organization by id
|
* Delete organization by id
|
||||||
* */
|
* */
|
||||||
const deleteOrganizationById = async (
|
const deleteOrganizationById = async ({
|
||||||
userId: string,
|
userId,
|
||||||
orgId: string,
|
authorizationHeader,
|
||||||
actorAuthMethod: ActorAuthMethod,
|
userAgentHeader,
|
||||||
actorOrgId: string | undefined
|
ipAddress,
|
||||||
) => {
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
authorizationHeader?: string;
|
||||||
|
userAgentHeader?: string;
|
||||||
|
ipAddress: string;
|
||||||
|
orgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string | undefined;
|
||||||
|
}) => {
|
||||||
const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin)
|
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
name: "DeleteOrganizationById",
|
name: "DeleteOrganizationById",
|
||||||
message: "Insufficient privileges"
|
message: "Insufficient privileges"
|
||||||
});
|
});
|
||||||
|
|
||||||
const organization = await orgDAL.deleteById(orgId);
|
|
||||||
if (organization.customerId) {
|
|
||||||
await licenseService.removeOrgCustomer(organization.customerId);
|
|
||||||
}
|
}
|
||||||
return organization;
|
|
||||||
|
if (!authorizationHeader) {
|
||||||
|
throw new UnauthorizedError({ name: "Authorization header not set on request." });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userAgentHeader) {
|
||||||
|
throw new BadRequestError({ name: "User agent not set on request." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = getConfig();
|
||||||
|
const authToken = authorizationHeader.replace("Bearer ", "");
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(authToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
|
||||||
|
if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" });
|
||||||
|
|
||||||
|
const response = await orgDAL.transaction(async (tx) => {
|
||||||
|
const projects = await projectDAL.find({ orgId }, { tx });
|
||||||
|
|
||||||
|
for await (const project of projects) {
|
||||||
|
await fnDeleteProjectSecretReminders(project.id, {
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
queueService,
|
||||||
|
projectBotService,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedOrg = await orgDAL.deleteById(orgId, tx);
|
||||||
|
|
||||||
|
if (deletedOrg.customerId) {
|
||||||
|
await licenseService.removeOrgCustomer(deletedOrg.customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new tokens without the organization ID present
|
||||||
|
const user = await userDAL.findById(userId, tx);
|
||||||
|
const { access: accessToken, refresh: refreshToken } = await loginService.generateUserTokens(
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
authMethod: decodedToken.authMethod,
|
||||||
|
ip: ipAddress,
|
||||||
|
userAgent: userAgentHeader,
|
||||||
|
isMfaVerified: decodedToken.isMfaVerified,
|
||||||
|
mfaMethod: decodedToken.mfaMethod
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
organization: deletedOrg,
|
||||||
|
tokens: {
|
||||||
|
accessToken,
|
||||||
|
refreshToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
* Org membership management
|
* Org membership management
|
||||||
|
@ -51,7 +51,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.where(`${TableName.Project}.orgId`, orgId)
|
.where(`${TableName.Project}.orgId`, orgId)
|
||||||
.andWhere((qb) => {
|
.andWhere((qb) => {
|
||||||
if (projectType) {
|
if (projectType !== "all") {
|
||||||
void qb.where(`${TableName.Project}.type`, projectType);
|
void qb.where(`${TableName.Project}.type`, projectType);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -8,12 +8,16 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
|
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
@ -28,13 +32,17 @@ import { TOrgServiceFactory } from "../org/org-service";
|
|||||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
@ -52,6 +60,9 @@ import {
|
|||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
TListProjectCertsDTO,
|
TListProjectCertsDTO,
|
||||||
TListProjectsDTO,
|
TListProjectsDTO,
|
||||||
|
TListProjectSshCasDTO,
|
||||||
|
TListProjectSshCertificatesDTO,
|
||||||
|
TListProjectSshCertificateTemplatesDTO,
|
||||||
TLoadProjectKmsBackupDTO,
|
TLoadProjectKmsBackupDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
TUpdateAuditLogsRetentionDTO,
|
TUpdateAuditLogsRetentionDTO,
|
||||||
@ -74,7 +85,10 @@ type TProjectServiceFactoryDep = {
|
|||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "insertMany" | "findByProjectId">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
identityProjectDAL: TIdentityProjectDALFactory;
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
@ -89,9 +103,14 @@ type TProjectServiceFactoryDep = {
|
|||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find">;
|
||||||
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
||||||
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
|
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||||
@ -112,9 +131,13 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
|
|
||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
projectQueue,
|
projectQueue,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
queueService,
|
||||||
|
projectBotService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
@ -132,6 +155,9 @@ export const projectServiceFactory = ({
|
|||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateTemplateDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@ -424,6 +450,14 @@ export const projectServiceFactory = ({
|
|||||||
await userDAL.deleteById(projectGhostUser.id, tx);
|
await userDAL.deleteById(projectGhostUser.id, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fnDeleteProjectSecretReminders(project.id, {
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
queueService,
|
||||||
|
projectBotService,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
|
||||||
return delProject;
|
return delProject;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -441,7 +475,12 @@ export const projectServiceFactory = ({
|
|||||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
||||||
|
|
||||||
if (includeRoles) {
|
if (includeRoles) {
|
||||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
const { permission } = await permissionService.getUserOrgPermission(
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
|
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
@ -896,6 +935,118 @@ export const projectServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of SSH CAs for project
|
||||||
|
*/
|
||||||
|
const listProjectSshCas = async ({
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
projectId
|
||||||
|
}: TListProjectSshCasDTO) => {
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const cas = await sshCertificateAuthorityDAL.find(
|
||||||
|
{
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
{ sort: [["updatedAt", "desc"]] }
|
||||||
|
);
|
||||||
|
|
||||||
|
return cas;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of SSH certificates for project
|
||||||
|
*/
|
||||||
|
const listProjectSshCertificates = async ({
|
||||||
|
limit = 25,
|
||||||
|
offset = 0,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
projectId
|
||||||
|
}: TListProjectSshCertificatesDTO) => {
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
|
|
||||||
|
const cas = await sshCertificateAuthorityDAL.find({
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificates = await sshCertificateDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
sshCaId: cas.map((ca) => ca.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||||
|
);
|
||||||
|
|
||||||
|
const count = await sshCertificateDAL.countSshCertificatesInProject(projectId);
|
||||||
|
|
||||||
|
return { certificates, totalCount: count };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of SSH certificate templates for project
|
||||||
|
*/
|
||||||
|
const listProjectSshCertificateTemplates = async ({
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
projectId
|
||||||
|
}: TListProjectSshCertificateTemplatesDTO) => {
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SSH);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const cas = await sshCertificateAuthorityDAL.find({
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateTemplates = await sshCertificateTemplateDAL.find({
|
||||||
|
$in: {
|
||||||
|
sshCaId: cas.map((ca) => ca.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificateTemplates };
|
||||||
|
};
|
||||||
|
|
||||||
const updateProjectKmsKey = async ({
|
const updateProjectKmsKey = async ({
|
||||||
projectId,
|
projectId,
|
||||||
kms,
|
kms,
|
||||||
@ -1129,6 +1280,9 @@ export const projectServiceFactory = ({
|
|||||||
listProjectAlerts,
|
listProjectAlerts,
|
||||||
listProjectPkiCollections,
|
listProjectPkiCollections,
|
||||||
listProjectCertificateTemplates,
|
listProjectCertificateTemplates,
|
||||||
|
listProjectSshCas,
|
||||||
|
listProjectSshCertificates,
|
||||||
|
listProjectSshCertificateTemplates,
|
||||||
updateVersionLimit,
|
updateVersionLimit,
|
||||||
updateAuditLogsRetention,
|
updateAuditLogsRetention,
|
||||||
updateProjectKmsKey,
|
updateProjectKmsKey,
|
||||||
|
@ -132,6 +132,13 @@ export type TGetProjectKmsKey = TProjectPermission;
|
|||||||
|
|
||||||
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||||
|
|
||||||
|
export type TListProjectSshCasDTO = TProjectPermission;
|
||||||
|
export type TListProjectSshCertificateTemplatesDTO = TProjectPermission;
|
||||||
|
export type TListProjectSshCertificatesDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectSlackConfig = TProjectPermission;
|
export type TGetProjectSlackConfig = TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectSlackConfig = {
|
export type TUpdateProjectSlackConfig = {
|
||||||
|
@ -5,6 +5,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
|||||||
|
|
||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||||
@ -16,6 +17,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
|||||||
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
||||||
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
|
||||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
||||||
@ -30,6 +32,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretFolderVersionDAL,
|
secretFolderVersionDAL,
|
||||||
|
secretDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL,
|
secretVersionV2DAL,
|
||||||
@ -37,6 +40,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||||
|
await secretDAL.pruneSecretReminders(queueService);
|
||||||
await auditLogDAL.pruneAuditLog();
|
await auditLogDAL.pruneAuditLog();
|
||||||
await identityAccessTokenDAL.removeExpiredTokens();
|
await identityAccessTokenDAL.removeExpiredTokens();
|
||||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||||
|
@ -5,6 +5,8 @@ import { TDbClient } from "@app/db";
|
|||||||
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
|
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
|
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
|
||||||
|
|
||||||
@ -339,6 +341,94 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pruneSecretReminders = async (queueService: TQueueServiceFactory) => {
|
||||||
|
const REMINDER_PRUNE_BATCH_SIZE = 5_000;
|
||||||
|
const MAX_RETRY_ON_FAILURE = 3;
|
||||||
|
let numberOfRetryOnFailure = 0;
|
||||||
|
let deletedReminderCount = 0;
|
||||||
|
|
||||||
|
logger.info(`${QueueName.DailyResourceCleanUp}: secret reminders started`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder);
|
||||||
|
const reminderJobs = repeatableJobs
|
||||||
|
.map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key }))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (reminderJobs.length === 0) {
|
||||||
|
logger.info(`${QueueName.DailyResourceCleanUp}: no reminder jobs found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) {
|
||||||
|
try {
|
||||||
|
const batchIds = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE).map((r) => r.secretId);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
$in: {
|
||||||
|
id: batchIds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
limit: REMINDER_PRUNE_BATCH_SIZE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find existing secrets with pagination
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const [secrets, secretsV2] = await Promise.all([
|
||||||
|
ormify(db, TableName.Secret).find(payload, opts),
|
||||||
|
ormify(db, TableName.SecretV2).find(payload, opts)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const foundSecretIds = new Set([
|
||||||
|
...secrets.map((secret) => secret.id),
|
||||||
|
...secretsV2.map((secret) => secret.id)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Find IDs that don't exist in either table
|
||||||
|
const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId));
|
||||||
|
|
||||||
|
// Delete reminders for non-existent secrets
|
||||||
|
for (const secretId of secretIdsNotFound) {
|
||||||
|
const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey;
|
||||||
|
|
||||||
|
if (jobKey) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey);
|
||||||
|
deletedReminderCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfRetryOnFailure = 0;
|
||||||
|
} catch (error) {
|
||||||
|
numberOfRetryOnFailure += 1;
|
||||||
|
logger.error(error, `Failed to process batch at offset ${offset}`);
|
||||||
|
|
||||||
|
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry the current batch
|
||||||
|
offset -= REMINDER_PRUNE_BATCH_SIZE;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between batches
|
||||||
|
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to complete secret reminder pruning");
|
||||||
|
} finally {
|
||||||
|
logger.info(
|
||||||
|
`${QueueName.DailyResourceCleanUp}: secret reminders completed. Deleted ${deletedReminderCount} reminders`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
update,
|
update,
|
||||||
@ -352,6 +442,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
findByBlindIndexes,
|
findByBlindIndexes,
|
||||||
upsertSecretReferences,
|
upsertSecretReferences,
|
||||||
findReferencedSecretReferences,
|
findReferencedSecretReferences,
|
||||||
findAllProjectSecretValues
|
findAllProjectSecretValues,
|
||||||
|
pruneSecretReminders
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user