Compare commits

...

135 Commits

Author SHA1 Message Date
f5a0641671 Add requirement for ssh issue credentials command to include either outFilePath or addToAgent flag 2024-12-19 11:55:44 -08:00
f97f98b2c3 Add ability to load retrieved ssh credentials into ssh agent with new addToAgent flag 2024-12-18 23:27:11 -08:00
89848a2f5c Merge pull request #2886 from Infisical/ssh-cli
CLI - SSH Capabilities
2024-12-18 14:12:06 -08:00
1936f7cc4f Merge pull request #2894 from Infisical/ssh-certs
Add OpenSSH dependency to standalone Dockerfiles
2024-12-18 11:55:46 -08:00
1adeb5a70d Add openssh dependency to standalone Dockerfiles 2024-12-18 11:51:13 -08:00
058475fc3f Ran go mod tidy 2024-12-18 11:45:09 -08:00
ee4eb7f84b Update Go SDK version dependency 2024-12-18 11:32:09 -08:00
8122433f5c Merge pull request #2893 from Infisical/ssh-certs
Expose ssh endpoints to api reference, update ssh sign/issue endpoints
2024-12-18 14:02:10 -05:00
a0411e3ba8 Expose ssh endpoints to api reference, update ssh sign/issue endpoints 2024-12-18 10:59:28 -08:00
f3cf1a3f50 Merge pull request #2830 from Infisical/ssh-certs
Infisical SSH (SSH Certificates)
2024-12-18 12:40:24 -05:00
b4b417658f Update ssh cli api error messages 2024-12-18 09:22:06 -08:00
fed99a14a8 Merge remote-tracking branch 'origin' into ssh-certs 2024-12-18 09:20:13 -08:00
d4cfee99a6 Update ssh docs 2024-12-18 09:20:02 -08:00
e70ca57510 update minor version 2024-12-18 10:48:11 -05:00
06f321e4bf Update Chart.yaml 2024-12-18 10:44:51 -05:00
3c3fcd0db8 update k8 version to 7.7 2024-12-18 10:44:37 -05:00
21eb2bed7e Merge pull request #2815 from Infisical/daniel/k8-push-secret
feat(k8-operator): push secrets
2024-12-18 00:26:55 -05:00
31a21a432d Update isValidKeyAlgorithm impl 2024-12-17 20:40:03 -08:00
381960b0bd Add docs for Infisical SSH 2024-12-17 20:35:02 -08:00
7eb05afe2a misc: better condition naming 2024-12-18 04:09:44 +01:00
0b54948b15 fix: better condition error 2024-12-18 04:09:44 +01:00
39e598e408 fix: move fixes from different branch 2024-12-18 04:09:44 +01:00
b735618601 fix(k8-operator): resource-based finalizer names 2024-12-18 04:09:44 +01:00
3a5e862def fix(k8-operator): helm and cleanup 2024-12-18 04:09:44 +01:00
d1c4a9c75a updated env slugs 2024-12-18 03:28:32 +01:00
5532844ee7 Added RBAC 2024-12-18 03:28:32 +01:00
dd5aab973f Update PROJECT 2024-12-18 03:28:32 +01:00
ced12baf5d Update conditions.go 2024-12-18 03:28:32 +01:00
7db1e62654 fix: requested changes 2024-12-18 03:28:32 +01:00
0ab3ae442e cleanup and resource seperation 2024-12-18 03:28:32 +01:00
ed9472efc8 remove print 2024-12-18 03:28:32 +01:00
e094844601 docs(k8-operator): push secrets 2024-12-18 03:28:32 +01:00
e761b49964 feat(k8-operator): push secrets 2024-12-18 03:28:32 +01:00
6a8be75b79 Merge pull request #2865 from Infisical/daniel/fix-reminder-cleanup
fix(secret-reminders): proper cleanup on deleted resources
2024-12-18 02:57:07 +01:00
a92e61575d fix: test types 2024-12-18 01:10:23 +01:00
761007208d misc: daily cleanup of rogue secret reminder jobs 2024-12-17 23:32:45 +01:00
cc3e0d1922 fix: remove completed and failed reminder jobs 2024-12-17 23:32:04 +01:00
765280eef6 Update ssh cli issue/sign according to review 2024-12-17 13:12:47 -08:00
215761ca6b Merge pull request #2858 from Infisical/daniel/azure-label
feat(azure-app-integration): label & reference support
2024-12-17 20:56:53 +01:00
0977ff1e36 Enforce min length on certificate template id param for ssh issue/sign operations 2024-12-17 11:13:22 -08:00
c6081900a4 Merge pull request #2885 from Infisical/misc/consolidated-missing-helm-conditions-for-namespace-installation
misc: added missing helm configs for namespace installation
2024-12-17 14:11:21 -05:00
86800c0cdb Update ssh issue/sign fns to be based on certificate template id 2024-12-17 10:37:05 -08:00
1fa99e5585 Begin docs for ssh 2024-12-17 10:16:10 -08:00
8f5bb44ff4 Merge pull request #2890 from akhilmhdh/fix/broken-breakcrumb 2024-12-17 09:38:30 -05:00
3f70f08e8c doc: added rationale for namespace installation 2024-12-17 22:15:54 +08:00
078eaff164 Merge pull request #2891 from Infisical/misc/remove-encrypted-data-key-from-org-response
misc: remove encrypted data key from org response
2024-12-17 21:45:28 +08:00
221aa99374 Merge pull request #2892 from Pranav2612000/improv/2845-dont-close-modal-on-outside-click-while-adding-secret
improv ui: don't close "Create Secrets" modal when clicking outside it
2024-12-17 19:08:43 +05:30
6a681dcf6a improv ux: don't close 'Create secrets' modal when clicking outside it
Fixes #2845
2024-12-17 19:02:50 +05:30
b99b98b6a4 misc: remove encrypted data key from org response 2024-12-17 21:24:56 +08:00
d7271b9631 improv ui: use radix modal mode for Modals
Using the modal mode ensures that interaction with outside elements
is disabled ( for e.g scroll ) and only dialog content is visible to
screen readers.
2024-12-17 18:49:37 +05:30
379e526200 Merge pull request #2888 from Infisical/fix/false-org-error
fix: resolves a false org not logged in error
2024-12-17 16:53:09 +05:30
=
1f151a9b05 feat: resolved broken breakcrumb in secret manager 2024-12-17 16:49:05 +05:30
6b2eb9c6c9 fix: resolves a false org not logged in error 2024-12-17 14:37:41 +05:30
be36827392 Finish ssh cli sign/issue commands 2024-12-16 17:42:11 -08:00
68a3291235 misc: requested changes 2024-12-16 23:24:08 +01:00
471f47d260 Fix ssh ca page backward redirect link 2024-12-16 12:26:37 -08:00
ccb757ec3e fix: missed transaction 2024-12-16 20:58:56 +01:00
b669b0a9f8 Merge pull request #2883 from Infisical/feat/sync-circle-ci-context
feat: circle ci context integration
2024-12-17 02:12:32 +08:00
9e768640cd misc: made scope project the default 2024-12-17 00:12:25 +08:00
35f7420447 misc: added missing helm configs 2024-12-16 23:54:43 +08:00
c6a0e36318 fix(api): secret reminders not getting deleted 2024-12-16 15:55:15 +01:00
181ba75f2a fix(dashboard): creation of new org when user is apart of no orgs 2024-12-16 15:55:14 +01:00
c00f6601bd fix(secrets-api): deletion of secret reminders on secret delete 2024-12-16 15:53:56 +01:00
111605a945 fix: ui improvement 2024-12-16 15:32:38 +01:00
2ac110f00e fix: requested changes 2024-12-16 15:32:38 +01:00
0366506213 feat(azure-app-integration): label & reference support 2024-12-16 15:32:38 +01:00
e3d29b637d misc: added type assertion 2024-12-16 22:27:29 +08:00
9cd0dc8970 Merge pull request #2884 from akhilmhdh/fix/group-access-failing 2024-12-16 09:25:01 -05:00
f8f5000bad misc: addressed review comments 2024-12-16 22:20:59 +08:00
40919ccf59 misc: finalized docs and other details 2024-12-16 20:15:14 +08:00
=
44303aca6a fix: group only access to project failing 2024-12-16 16:09:05 +05:30
4bd50c3548 misc: unified to a single integration 2024-12-16 16:08:51 +08:00
fb253d00eb Move ssh out to org level 2024-12-15 20:43:13 -08:00
097512c691 Begin adding ssh commands to cli 2024-12-15 17:30:57 -08:00
64a982d5e0 Merge pull request #2876 from akhilmhdh/feat/split-project
feat: changed multi insert into batch insert
2024-12-13 14:52:48 -05:00
=
1080438ad8 feat: changed multi insert into batch insert 2024-12-14 01:19:56 +05:30
eb3acae332 Merge pull request #2868 from akhilmhdh/feat/split-project
One slice - 3 Projects
2024-12-13 14:36:58 -05:00
=
a0b3520899 feat: updated rollback 2024-12-14 01:00:12 +05:30
2f6f359ddf Merge pull request #2846 from Infisical/misc/operator-namespace-installation
feat: k8 operator namespace installation
2024-12-13 14:10:45 -05:00
=
df8c1e54e0 feat: review changes 2024-12-13 23:50:49 +05:30
=
cac060deff feat: added space 2024-12-13 21:38:44 +05:30
=
47269bc95b feat: resolved undefined redirect 2024-12-13 21:38:44 +05:30
=
8502e9a1d8 feat: removed console log 2024-12-13 21:38:43 +05:30
=
d89eb4fa84 feat: added check in workspace cert api 2024-12-13 21:38:43 +05:30
=
ca7ab4eaf1 feat: resolved typo in access control 2024-12-13 21:38:43 +05:30
=
c57fc5e3f1 feat: fixed review comments 2024-12-13 21:38:43 +05:30
=
9b4e1f561e feat: removed service token from migration and resolved failing migration on groups 2024-12-13 21:38:43 +05:30
=
097fcad5ae fix: resolved failing seed 2024-12-13 21:38:43 +05:30
=
d1547564f9 feat: run through check to all frontend urls 2024-12-13 21:38:43 +05:30
=
24acb98978 feat: project settings hiding 2024-12-13 21:38:42 +05:30
=
0fd8274ff0 feat: added project id mapping logic for cert and kms 2024-12-13 21:38:42 +05:30
=
a857375cc1 feat: fixed migration issues and resolved all routes in frontend 2024-12-13 21:38:42 +05:30
=
69bf9dc20f feat: completed migration 2024-12-13 21:38:42 +05:30
=
5151c91760 feat: check for cmek implemented 2024-12-13 21:38:42 +05:30
=
f12d8b6f89 feat: check for cert manager endpoints 2024-12-13 21:38:42 +05:30
=
695c499448 feat: added type for project and validation check for secret manager specific endpoints 2024-12-13 21:38:42 +05:30
1cbf030e6c Merge remote-tracking branch 'origin/main' into feat/sync-circle-ci-context 2024-12-13 22:34:06 +08:00
dc715cc238 Merge pull request #2874 from Infisical/misc/address-high-cpu-usage-from-secret-version-query
misc: address cpu usage issue of secret version query
2024-12-13 08:34:36 -05:00
d873f2e50f misc: address cpu usage issue of secret version query 2024-12-13 20:31:34 +08:00
16ea757928 Merge pull request #2857 from Infisical/feat/jwt-auth
feat: jwt auth
2024-12-13 14:15:43 +08:00
8713643bc1 misc: add support for number field values 2024-12-13 14:02:32 +08:00
c35657ed49 misc: addressed review comments 2024-12-13 13:39:23 +08:00
5b4487fae8 add period to secret share text 2024-12-12 16:04:51 -05:00
474731d8ef update share secret text 2024-12-12 16:02:30 -05:00
e9f254f81b Update azure-devops.mdx 2024-12-12 15:36:38 -05:00
a7b25f3bd8 misc: addressed module issue 2024-12-12 00:24:36 +08:00
7896b4e85e doc: added documentation 2024-12-12 00:23:06 +08:00
8d79fa3529 misc: finalized login logic and other ui/ux changes 2024-12-11 22:49:26 +08:00
b2efb2845a misc: finalized api endpoint schema 2024-12-11 21:09:25 +08:00
f5920f416a Merge remote-tracking branch 'origin' into ssh-certs 2024-12-10 12:46:14 -08:00
3b2154bab4 Add further input validation/sanitization for ssh params 2024-12-10 12:44:08 -08:00
7c8f2e5548 docs + minor fixes 2024-12-10 21:14:13 +01:00
9d9f6ec268 misc: initial ui work 2024-12-11 03:40:21 +08:00
c5816014a6 Add suggested PR review improvements, better validation on ssh cert template modal 2024-12-10 11:34:08 -08:00
a730b16318 fix circleCI name spacing 2024-12-10 20:12:55 +01:00
cc3d132f5d feat(integrations): New CircleCI Context Sync 2024-12-10 20:07:23 +01:00
56aab172d3 feat: added logic for jwt auth login 2024-12-11 00:05:31 +08:00
c8ee06341a feat: finished crud endpoints 2024-12-10 23:10:44 +08:00
48174e2500 security + performance improvements to ssh fns 2024-12-09 22:22:54 -08:00
7cf297344b Move ssh back to project level 2024-12-09 21:36:42 -08:00
84c26581a6 feat: jwt auth setup 2024-12-10 02:41:04 +08:00
42249726d4 Make PR review adjustments, ssh ca public key endpoint, ssh cert template status 2024-12-08 21:23:00 -08:00
ec1ce3dc06 Fix type issues 2024-12-05 23:16:31 -08:00
82a4b89bb5 Fix invalid file path for ssh 2024-12-05 23:09:04 -08:00
ff3d8c896b Fix frontend lint issues 2024-12-05 23:06:04 -08:00
6e720c2f64 Add SSH certificate tab + data structure 2024-12-05 23:01:28 -08:00
5b618b07fa Add sign SSH key operation to frontend 2024-12-04 20:13:30 -08:00
a5a1f57284 Fix issued ssh cert defaul ttl 2024-12-04 18:38:14 -08:00
8327f6154e Add openssh dependency onto production Dockerfile 2024-12-03 23:25:22 -08:00
20a9fc113c Update ttl field label on ssh template modal 2024-12-03 23:23:39 -08:00
8edfa9ad0b Improve requested user/host validation for ssh certificate template 2024-12-03 23:22:04 -08:00
00ce755996 Fix type issues 2024-12-03 22:38:29 -08:00
3b2173a098 Add issue SSH certificate modal 2024-12-03 18:36:32 -08:00
07d9398aad Add permissioning to SSH, add publicKey return for SSH CA, polish 2024-12-03 17:38:23 -08:00
4fc8c509ac Finish preliminary loop on SSH certificates 2024-12-02 22:37:23 -08:00
337 changed files with 17322 additions and 2852 deletions

View File

@ -137,6 +137,7 @@ RUN apt-get update && apt-get install -y \
freetds-dev \
freetds-bin \
tdsodbc \
openssh \
&& rm -rf /var/lib/apt/lists/*
# Configure ODBC in production

View File

@ -139,7 +139,8 @@ RUN apk --update add \
freetds-dev \
bash \
curl \
git
git \
openssh
# 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

View File

@ -7,7 +7,8 @@ WORKDIR /app
RUN apk --update add \
python3 \
make \
g++
g++ \
openssh
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \

View File

@ -17,7 +17,8 @@ RUN apk --update add \
openssl-dev \
python3 \
make \
g++
g++ \
openssh
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \

View File

@ -22,8 +22,10 @@ export const mockQueue = (): TQueueServiceFactory => {
listen: (name, event) => {
events[name] = event;
},
getRepeatableJobs: async () => [],
clearQueue: async () => {},
stopJobById: async () => {},
stopRepeatableJobByJobId: async () => true
stopRepeatableJobByJobId: async () => true,
stopRepeatableJobByKey: async () => true
};
};

View File

@ -31,6 +31,8 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-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 { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
@ -52,6 +54,7 @@ import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-acces
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@ -162,6 +165,7 @@ declare module "fastify" {
identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory;
identityJwtAuth: TIdentityJwtAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
@ -175,6 +179,8 @@ declare module "fastify" {
auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory;
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory;

View File

@ -98,6 +98,9 @@ import {
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
TIdentityJwtAuths,
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate,
TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate,
@ -199,6 +202,9 @@ import {
TProjectSlackConfigs,
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectSplitBackfillIds,
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate,
TProjectsUpdate,
TProjectTemplates,
TProjectTemplatesInsert,
@ -311,6 +317,21 @@ import {
TSlackIntegrations,
TSlackIntegrationsInsert,
TSlackIntegrationsUpdate,
TSshCertificateAuthorities,
TSshCertificateAuthoritiesInsert,
TSshCertificateAuthoritiesUpdate,
TSshCertificateAuthoritySecrets,
TSshCertificateAuthoritySecretsInsert,
TSshCertificateAuthoritySecretsUpdate,
TSshCertificateBodies,
TSshCertificateBodiesInsert,
TSshCertificateBodiesUpdate,
TSshCertificates,
TSshCertificatesInsert,
TSshCertificatesUpdate,
TSshCertificateTemplates,
TSshCertificateTemplatesInsert,
TSshCertificateTemplatesUpdate,
TSuperAdmin,
TSuperAdminInsert,
TSuperAdminUpdate,
@ -372,6 +393,31 @@ declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[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<
TCertificateAuthorities,
TCertificateAuthoritiesInsert,
@ -590,6 +636,11 @@ declare module "knex/types/tables" {
TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate
>;
[TableName.IdentityJwtAuth]: KnexOriginal.CompositeTableType<
TIdentityJwtAuths,
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate
>;
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
@ -830,5 +881,10 @@ declare module "knex/types/tables" {
TProjectTemplatesUpdate
>;
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
[TableName.ProjectSplitBackfillIds]: KnexOriginal.CompositeTableType<
TProjectSplitBackfillIds,
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate
>;
}
}

View File

@ -0,0 +1,34 @@
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.IdentityJwtAuth))) {
await knex.schema.createTable(TableName.IdentityJwtAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("configurationType").notNullable();
t.string("jwksUrl").notNullable();
t.binary("encryptedJwksCaCert").notNullable();
t.binary("encryptedPublicKeys").notNullable();
t.string("boundIssuer").notNullable();
t.string("boundAudiences").notNullable();
t.jsonb("boundClaims").notNullable();
t.string("boundSubject").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.IdentityJwtAuth);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityJwtAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityJwtAuth);
}

View File

@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.index("folderId");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.dropIndex("folderId");
});
}
}

View File

@ -0,0 +1,297 @@
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { v4 as uuidV4 } from "uuid";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ProjectType, TableName } from "../schemas";
/* eslint-disable no-await-in-loop,@typescript-eslint/ban-ts-comment */
const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => {
const newProjectId = uuidV4();
const project = await knex(TableName.Project).where("id", projectId).first();
await knex(TableName.Project).insert({
...project,
type: projectType,
// @ts-ignore id is required
id: newProjectId,
slug: slugify(`${project?.name}-${alphaNumericNanoId(4)}`)
});
const customRoleMapping: Record<string, string> = {};
const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId);
if (projectCustomRoles.length) {
await knex.batchInsert(
TableName.ProjectRoles,
projectCustomRoles.map((el) => {
const id = uuidV4();
customRoleMapping[el.id] = id;
return {
...el,
id,
projectId: newProjectId,
permissions: el.permissions ? JSON.stringify(el.permissions) : el.permissions
};
})
);
}
const groupMembershipMapping: Record<string, string> = {};
const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId);
if (groupMemberships.length) {
await knex.batchInsert(
TableName.GroupProjectMembership,
groupMemberships.map((el) => {
const id = uuidV4();
groupMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn(
"projectMembershipId",
groupMemberships.map((el) => el.id)
);
if (groupMembershipRoles.length) {
await knex.batchInsert(
TableName.GroupProjectMembershipRole,
groupMembershipRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = groupMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const identityProjectMembershipMapping: Record<string, string> = {};
const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId);
if (identities.length) {
await knex.batchInsert(
TableName.IdentityProjectMembership,
identities.map((el) => {
const id = uuidV4();
identityProjectMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn(
"projectMembershipId",
identities.map((el) => el.id)
);
if (identitiesRoles.length) {
await knex.batchInsert(
TableName.IdentityProjectMembershipRole,
identitiesRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const projectMembershipMapping: Record<string, string> = {};
const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId);
if (projectUserMembers.length) {
await knex.batchInsert(
TableName.ProjectMembership,
projectUserMembers.map((el) => {
const id = uuidV4();
projectMembershipMapping[el.id] = id;
return { ...el, id, projectId: newProjectId };
})
);
}
const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn(
"projectMembershipId",
projectUserMembers.map((el) => el.id)
);
if (membershipRoles.length) {
await knex.batchInsert(
TableName.ProjectUserMembershipRole,
membershipRoles.map((el) => {
const id = uuidV4();
const projectMembershipId = projectMembershipMapping[el.projectMembershipId];
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
return { ...el, id, projectMembershipId, customRoleId };
})
);
}
const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true);
if (kmsKeys.length) {
await knex.batchInsert(
TableName.KmsKey,
kmsKeys.map((el) => {
const id = uuidV4();
const slug = slugify(alphaNumericNanoId(8).toLowerCase());
return { ...el, id, slug, projectId: newProjectId };
})
);
}
const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first();
if (projectBot) {
const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId };
await knex(TableName.ProjectBot).insert(newProjectBot);
}
const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId);
if (projectKeys.length) {
await knex.batchInsert(
TableName.ProjectKeys,
projectKeys.map((el) => {
const id = uuidV4();
return { ...el, id, projectId: newProjectId };
})
);
}
return newProjectId;
};
const BATCH_SIZE = 500;
export async function up(knex: Knex): Promise<void> {
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
if (!hasSplitMappingTable) {
await knex.schema.createTable(TableName.ProjectSplitBackfillIds, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("sourceProjectId", 36).notNullable();
t.foreign("sourceProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.string("destinationProjectType").notNullable();
t.string("destinationProjectId", 36).notNullable();
t.foreign("destinationProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
if (!hasTypeColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("type");
});
let projectsToBeTyped;
do {
// eslint-disable-next-line no-await-in-loop
projectsToBeTyped = await knex(TableName.Project).whereNull("type").limit(BATCH_SIZE).select("id");
if (projectsToBeTyped.length) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.Project)
.whereIn(
"id",
projectsToBeTyped.map((el) => el.id)
)
.update({ type: ProjectType.SecretManager });
}
} while (projectsToBeTyped.length > 0);
const projectsWithCertificates = await knex(TableName.CertificateAuthority)
.distinct("projectId")
.select("projectId");
/* eslint-disable no-await-in-loop,no-param-reassign */
for (const { projectId } of projectsWithCertificates) {
const newProjectId = await newProject(knex, projectId, ProjectType.CertificateManager);
await knex(TableName.CertificateAuthority).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.PkiAlert).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.PkiCollection).where("projectId", projectId).update({ projectId: newProjectId });
await knex(TableName.ProjectSplitBackfillIds).insert({
sourceProjectId: projectId,
destinationProjectType: ProjectType.CertificateManager,
destinationProjectId: newProjectId
});
}
const projectsWithCmek = await knex(TableName.KmsKey)
.where("isReserved", false)
.whereNotNull("projectId")
.distinct("projectId")
.select("projectId");
for (const { projectId } of projectsWithCmek) {
if (projectId) {
const newProjectId = await newProject(knex, projectId, ProjectType.KMS);
await knex(TableName.KmsKey)
.where({
isReserved: false,
projectId
})
.update({ projectId: newProjectId });
await knex(TableName.ProjectSplitBackfillIds).insert({
sourceProjectId: projectId,
destinationProjectType: ProjectType.KMS,
destinationProjectId: newProjectId
});
}
}
/* eslint-enable */
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("type").notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
if (hasTypeColumn && hasSplitMappingTable) {
const splitProjectMappings = await knex(TableName.ProjectSplitBackfillIds).where({});
const certMapping = splitProjectMappings.filter(
(el) => el.destinationProjectType === ProjectType.CertificateManager
);
/* eslint-disable no-await-in-loop */
for (const project of certMapping) {
await knex(TableName.CertificateAuthority)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
await knex(TableName.PkiAlert)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
await knex(TableName.PkiCollection)
.where("projectId", project.destinationProjectId)
.update({ projectId: project.sourceProjectId });
}
/* eslint-enable */
const kmsMapping = splitProjectMappings.filter((el) => el.destinationProjectType === ProjectType.KMS);
/* eslint-disable no-await-in-loop */
for (const project of kmsMapping) {
await knex(TableName.KmsKey)
.where({
isReserved: false,
projectId: project.destinationProjectId
})
.update({ projectId: project.sourceProjectId });
}
/* eslint-enable */
await knex(TableName.ProjectMembership)
.whereIn(
"projectId",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex(TableName.ProjectRoles)
.whereIn(
"projectId",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex(TableName.Project)
.whereIn(
"id",
splitProjectMappings.map((el) => el.destinationProjectId)
)
.delete();
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("type");
});
}
if (hasSplitMappingTable) {
await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds);
}
}

View 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);
}

View File

@ -0,0 +1,33 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const IdentityJwtAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
identityId: z.string().uuid(),
configurationType: z.string(),
jwksUrl: z.string(),
encryptedJwksCaCert: zodBuffer,
encryptedPublicKeys: zodBuffer,
boundIssuer: z.string(),
boundAudiences: z.string(),
boundClaims: z.unknown(),
boundSubject: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityJwtAuths = z.infer<typeof IdentityJwtAuthsSchema>;
export type TIdentityJwtAuthsInsert = Omit<z.input<typeof IdentityJwtAuthsSchema>, TImmutableDBKeys>;
export type TIdentityJwtAuthsUpdate = Partial<Omit<z.input<typeof IdentityJwtAuthsSchema>, TImmutableDBKeys>>;

View File

@ -30,6 +30,7 @@ export * from "./identity-access-tokens";
export * from "./identity-aws-auths";
export * from "./identity-azure-auths";
export * from "./identity-gcp-auths";
export * from "./identity-jwt-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-metadata";
export * from "./identity-oidc-auths";
@ -64,6 +65,7 @@ export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./project-slack-configs";
export * from "./project-split-backfill-ids";
export * from "./project-templates";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
@ -105,6 +107,11 @@ export * from "./secrets";
export * from "./secrets-v2";
export * from "./service-tokens";
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 "./totp-configs";
export * from "./trusted-ips";

View File

@ -2,6 +2,11 @@ import { z } from "zod";
export enum TableName {
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",
CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs",
@ -68,6 +73,7 @@ export enum TableName {
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths",
IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
@ -105,6 +111,7 @@ export enum TableName {
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
ProjectSplitBackfillIds = "project_split_backfill_ids",
// junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction",
@ -196,5 +203,13 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth"
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth"
}
export enum ProjectType {
SecretManager = "secret-manager",
CertificateManager = "cert-manager",
KMS = "kms",
SSH = "ssh"
}

View File

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectSplitBackfillIdsSchema = z.object({
id: z.string().uuid(),
sourceProjectId: z.string(),
destinationProjectType: z.string(),
destinationProjectId: z.string()
});
export type TProjectSplitBackfillIds = z.infer<typeof ProjectSplitBackfillIdsSchema>;
export type TProjectSplitBackfillIdsInsert = Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>;
export type TProjectSplitBackfillIdsUpdate = Partial<
Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>
>;

View File

@ -24,7 +24,8 @@ export const ProjectsSchema = z.object({
auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional()
description: z.string().nullable().optional(),
type: z.string()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View 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>
>;

View 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>
>;

View 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>>;

View 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>
>;

View 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>>;

View File

@ -4,7 +4,7 @@ import { Knex } from "knex";
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@ -24,6 +24,7 @@ export async function seed(knex: Knex): Promise<void> {
name: seedData1.project.name,
orgId: seedData1.organization.id,
slug: "first-project",
type: ProjectType.SecretManager,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.project.id

View File

@ -1,6 +1,6 @@
import { Knex } from "knex";
import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas";
import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
@ -16,6 +16,7 @@ export async function seed(knex: Knex): Promise<void> {
orgId: seedData1.organization.id,
slug: seedData1.projectV3.slug,
version: ProjectVersion.V3,
type: ProjectType.SecretManager,
// eslint-disable-next-line
// @ts-ignore
id: seedData1.projectV3.id

View File

@ -25,6 +25,9 @@ import { registerSecretRotationRouter } from "./secret-rotation-router";
import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSecretVersionRouter } from "./secret-version-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 { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
@ -68,6 +71,15 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ 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(
async (ssoRouter) => {
await ssoRouter.register(registerSamlRouter);

View 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
};
}
});
};

View 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
};
}
});
};

View 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;
}
});
};

View File

@ -1,5 +1,6 @@
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 { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@ -86,13 +87,15 @@ export const accessApprovalPolicyServiceFactory = ({
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@ -190,14 +193,7 @@ export const accessApprovalPolicyServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
// Anyone in the project should be able to get the policies.
/* const { permission } = */ await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId);
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
return accessApprovalPolicies;
@ -241,13 +237,14 @@ export const accessApprovalPolicyServiceFactory = ({
if (!accessApprovalPolicy) {
throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
accessApprovalPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
@ -324,13 +321,14 @@ export const accessApprovalPolicyServiceFactory = ({
const policy = await accessApprovalPolicyDAL.findById(policyId);
if (!policy) throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
policy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval

View File

@ -2,9 +2,12 @@ import {
TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO
} 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 { TProjectPermission } from "@app/lib/types";
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 { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
@ -95,6 +98,11 @@ export enum EventType {
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth",
ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth",
UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth",
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
@ -138,6 +146,17 @@ export enum EventType {
SECRET_APPROVAL_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
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",
GET_CA = "get-certificate-authority",
UPDATE_CA = "update-certificate-authority",
@ -903,6 +922,67 @@ interface GetIdentityOidcAuthEvent {
};
}
interface LoginIdentityJwtAuthEvent {
type: EventType.LOGIN_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
identityJwtAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityJwtAuthEvent {
type: EventType.ADD_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
configurationType: string;
jwksUrl?: string;
jwksCaCert: string;
publicKeys: string[];
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface UpdateIdentityJwtAuthEvent {
type: EventType.UPDATE_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
configurationType?: string;
jwksUrl?: string;
jwksCaCert?: string;
publicKeys?: string[];
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityJwtAuthEvent {
type: EventType.REVOKE_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
};
}
interface GetIdentityJwtAuthEvent {
type: EventType.GET_IDENTITY_JWT_AUTH;
metadata: {
identityId: string;
};
}
interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT;
metadata: {
@ -1140,6 +1220,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 {
type: EventType.CREATE_CA;
metadata: {
@ -1742,6 +1933,11 @@ export type Event =
| DeleteIdentityOidcAuthEvent
| UpdateIdentityOidcAuthEvent
| GetIdentityOidcAuthEvent
| LoginIdentityJwtAuthEvent
| AddIdentityJwtAuthEvent
| UpdateIdentityJwtAuthEvent
| GetIdentityJwtAuthEvent
| DeleteIdentityJwtAuthEvent
| CreateEnvironmentEvent
| GetEnvironmentEvent
| UpdateEnvironmentEvent
@ -1766,6 +1962,17 @@ export type Event =
| SecretApprovalClosed
| SecretApprovalRequest
| SecretApprovalReopened
| SignSshKey
| IssueSshCreds
| CreateSshCa
| GetSshCa
| UpdateSshCa
| DeleteSshCa
| GetSshCaCertificateTemplates
| CreateSshCertificateTemplate
| UpdateSshCertificateTemplate
| GetSshCertificateTemplate
| DeleteSshCertificateTemplate
| CreateCa
| GetCa
| UpdateCa

View File

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms";
import { SecretKeyEncoding } from "@app/db/schemas";
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
@ -67,13 +67,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -146,13 +147,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -225,13 +227,14 @@ export const dynamicSecretLeaseServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.Lease,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })

View File

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
@ -73,13 +73,14 @@ export const dynamicSecretServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.CreateRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -144,13 +145,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -227,13 +229,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })

View File

@ -269,6 +269,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
@ -284,13 +285,15 @@ export const permissionDALFactory = (db: TDbClient) => {
membershipCreatedAt,
groupMembershipCreatedAt,
groupMembershipUpdatedAt,
membershipUpdatedAt
membershipUpdatedAt,
projectType
}) => ({
orgId,
orgAuthEnforced,
userId,
projectId,
username,
projectType,
id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
@ -449,6 +452,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
@ -480,7 +484,14 @@ export const permissionDALFactory = (db: TDbClient) => {
const permission = sqlNestRelationships({
data: docs,
key: "membershipId",
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
parentMapper: ({
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
orgId,
identityName,
projectType
}) => ({
id: membershipId,
identityId,
username: identityName,
@ -488,6 +499,7 @@ export const permissionDALFactory = (db: TDbClient) => {
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
orgId,
projectType,
// just a prefilled value
orgAuthEnforced: false
}),

View File

@ -6,6 +6,7 @@ import handlebars from "handlebars";
import {
OrgMembershipRole,
ProjectMembershipRole,
ProjectType,
ServiceTokenScopes,
TIdentityProjectMemberships,
TProjectMemberships
@ -255,6 +256,13 @@ export const permissionServiceFactory = ({
return {
permission,
membership: userProjectPermission,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== userProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
});
}
},
hasRole: (role: string) =>
userProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@ -323,6 +331,13 @@ export const permissionServiceFactory = ({
return {
permission,
membership: identityProjectPermission,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== identityProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
});
}
},
hasRole: (role: string) =>
identityProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@ -361,7 +376,14 @@ export const permissionServiceFactory = ({
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
membership: undefined
membership: undefined,
ForbidOnInvalidProjectType: (productType: ProjectType) => {
if (productType !== serviceTokenProject.type) {
throw new BadRequestError({
message: `The project is of type ${serviceTokenProject.type}. Operations of type ${productType} are not allowed.`
});
}
}
};
};
@ -370,6 +392,7 @@ export const permissionServiceFactory = ({
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: undefined;
hasRole: (arg: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
} // service token doesn't have both membership and roles
: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
@ -379,6 +402,7 @@ export const permissionServiceFactory = ({
roles: Array<{ role: string }>;
};
hasRole: (role: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
};
const getProjectPermission = async <T extends ActorType>(

View File

@ -54,6 +54,9 @@ export enum ProjectPermissionSub {
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates",
CertificateTemplates = "certificate-templates",
SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificates = "ssh-certificates",
SshCertificateTemplates = "ssh-certificate-templates",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms",
@ -132,6 +135,9 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
@ -338,6 +344,28 @@ const GeneralPermissionSchema = [
"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({
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
@ -480,7 +508,10 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Certificates,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates
].forEach((el) => {
can(
[
@ -665,6 +696,11 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
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(
[
ProjectPermissionCmekActions.Create,
@ -707,6 +743,9 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
return rules;
};

View File

@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import picomatch from "picomatch";
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";
@ -78,13 +79,14 @@ export const secretApprovalPolicyServiceFactory = ({
if (!groupApprovers.length && approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@ -191,13 +193,14 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
secretApprovalPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
const plan = await licenseService.getPlan(actorOrgId);
@ -285,13 +288,14 @@ export const secretApprovalPolicyServiceFactory = ({
if (!sapPolicy)
throw new NotFoundError({ message: `Secret approval policy with ID '${secretPolicyId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
sapPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval

View File

@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import {
ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretType,
@ -875,13 +876,14 @@ export const secretApprovalRequestServiceFactory = ({
}: TGenerateSecretApprovalRequestDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@ -1155,14 +1157,14 @@ export const secretApprovalRequestServiceFactory = ({
if (actor === ActorType.SERVICE || actor === ActorType.Machine)
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new NotFoundError({

View File

@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
import { ProjectVersion, TableName } from "@app/db/schemas";
import { ProjectType, ProjectVersion, TableName } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
@ -53,13 +53,14 @@ export const secretRotationServiceFactory = ({
actorAuthMethod,
projectId
}: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
return {
@ -81,13 +82,14 @@ export const secretRotationServiceFactory = ({
secretPath,
environment
}: TCreateSecretRotationDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
@ -234,13 +236,14 @@ export const secretRotationServiceFactory = ({
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
doc.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
@ -251,13 +254,14 @@ export const secretRotationServiceFactory = ({
const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new NotFoundError({ message: `Rotation with ID '${rotationId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
doc.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation

View File

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { ProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
@ -322,13 +322,14 @@ export const secretSnapshotServiceFactory = ({
if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` });
const shouldUseBridge = snapshot.projectVersion === 3;
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
snapshot.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback

View File

@ -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 };
};

View File

@ -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
});

View File

@ -0,0 +1,248 @@
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
};
};

View File

@ -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">;

View File

@ -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);
};

View File

@ -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;
};

View File

@ -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
};
};

View File

@ -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
});

View 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;
};

View 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(() => {});
}
};

View File

@ -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
});

View File

@ -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;
};

View 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
};
};

View File

@ -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;
};

View File

@ -351,6 +351,52 @@ export const OIDC_AUTH = {
}
} as const;
export const JWT_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
configurationType: "The configuration for validating JWTs. Must be one of: 'jwks', 'static'",
jwksUrl:
"The URL of the JWKS endpoint. Required if configurationType is 'jwks'. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.",
jwksCaCert: "The PEM-encoded CA certificate for validating the TLS connection to the JWKS endpoint.",
publicKeys:
"A list of PEM-encoded public keys used to verify JWT signatures. Required if configurationType is 'static'. Each key must be in RSA or ECDSA format and properly PEM-encoded with BEGIN/END markers.",
boundIssuer: "The unique identifier of the JWT provider.",
boundAudiences: "The list of intended recipients.",
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
boundSubject: "The expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
configurationType: "The new configuration for validating JWTs. Must be one of: 'jwks', 'static'",
jwksUrl:
"The new URL of the JWKS endpoint. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.",
jwksCaCert: "The new PEM-encoded CA certificate for validating the TLS connection to the JWKS endpoint.",
publicKeys:
"A new list of PEM-encoded public keys used to verify JWT signatures. Each key must be in RSA or ECDSA format and properly PEM-encoded with BEGIN/END markers.",
boundIssuer: "The new unique identifier of the JWT provider.",
boundAudiences: "The new list of intended recipients.",
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
boundSubject: "The new expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
}
} as const;
export const ORGANIZATIONS = {
LIST_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to get memberships from."
@ -382,7 +428,8 @@ export const ORGANIZATIONS = {
search: "The text string that identity membership names will be filtered by."
},
GET_PROJECTS: {
organizationId: "The ID of the organization to get projects from."
organizationId: "The ID of the organization to get projects from.",
type: "The type of project to filter by."
},
LIST_GROUPS: {
organizationId: "The ID of the organization to list groups for."
@ -445,6 +492,17 @@ export const PROJECTS = {
LIST_INTEGRATION_AUTHORIZATION: {
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: {
slug: "The slug of the project to list CAs for.",
status: "The status of the CA to filter by.",
@ -1079,6 +1137,7 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.",
azureLabel: "Define which label to assign to secrets created in Azure App Configuration.",
githubVisibility:
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
githubVisibilityRepoIds:
@ -1139,6 +1198,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 = {
CREATE: {
projectSlug: "Slug of the project to create the CA in.",

View File

@ -20,11 +20,12 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
export type TFindFilter<R extends object = object> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>;
$notNull?: Array<keyof R>;
$search?: Partial<{ [k in keyof R]: R[k] }>;
$complex?: TKnexDynamicOperator<R>;
};
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>) => {
void bd.where(filter);
if ($in) {
@ -34,6 +35,13 @@ export const buildFindFilter =
}
});
}
if ($notNull?.length) {
$notNull.forEach((key) => {
void bd.whereNotNull(key as never);
});
}
if ($search) {
Object.entries($search).forEach(([key, val]) => {
if (val) {

View File

@ -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 q = queueContainer[name];
const job = await q.getJob(jobId);
@ -326,6 +333,11 @@ export const queueServiceFactory = (
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 q = queueContainer[name];
const job = await q.getJob(jobId);
@ -349,8 +361,10 @@ export const queueServiceFactory = (
shutdown,
stopRepeatableJob,
stopRepeatableJobByJobId,
stopRepeatableJobByKey,
clearQueue,
stopJobById,
getRepeatableJobs,
startPg,
queuePg
};

View File

@ -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 { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-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 { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
@ -121,6 +128,8 @@ import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/i
import { identityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-dal";
import { identityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
@ -298,6 +307,7 @@ export const registerRoutes = async (
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(auditLogDb ?? db);
@ -342,6 +352,12 @@ export const registerRoutes = async (
const dynamicSecretDAL = dynamicSecretDALFactory(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 internalKmsDAL = internalKmsDALFactory(db);
const externalKmsDAL = externalKmsDALFactory(db);
@ -535,7 +551,11 @@ export const registerRoutes = async (
const orgService = orgServiceFactory({
userAliasDAL,
queueService,
identityMetadataDAL,
secretDAL,
secretV2BridgeDAL,
folderDAL,
licenseService,
samlConfigDAL,
orgRoleDAL,
@ -556,6 +576,7 @@ export const registerRoutes = async (
groupDAL,
orgBotDAL,
oidcConfigDAL,
loginService,
projectBotService
});
const signupService = authSignupServiceFactory({
@ -704,6 +725,22 @@ export const registerRoutes = async (
queueService
});
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL,
sshCertificateTemplateDAL,
sshCertificateDAL,
sshCertificateBodyDAL,
kmsService,
permissionService
});
const sshCertificateTemplateService = sshCertificateTemplateServiceFactory({
sshCertificateTemplateDAL,
sshCertificateAuthorityDAL,
permissionService
});
const certificateAuthorityService = certificateAuthorityServiceFactory({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
@ -754,7 +791,8 @@ export const registerRoutes = async (
pkiAlertDAL,
pkiCollectionDAL,
permissionService,
smtpService
smtpService,
projectDAL
});
const pkiCollectionService = pkiCollectionServiceFactory({
@ -762,7 +800,8 @@ export const registerRoutes = async (
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
permissionService,
projectDAL
});
const projectTemplateService = projectTemplateServiceFactory({
@ -771,10 +810,58 @@ export const registerRoutes = async (
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({
permissionService,
projectDAL,
secretDAL,
secretV2BridgeDAL,
queueService,
projectQueue: projectQueueService,
projectBotService,
identityProjectDAL,
identityOrgMembershipDAL,
projectKeyDAL,
@ -790,6 +877,9 @@ export const registerRoutes = async (
certificateDAL,
pkiAlertDAL,
pkiCollectionDAL,
sshCertificateAuthorityDAL,
sshCertificateDAL,
sshCertificateTemplateDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
keyStore,
@ -854,48 +944,6 @@ export const registerRoutes = async (
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({
licenseService,
projectBotService,
@ -1184,6 +1232,15 @@ export const registerRoutes = async (
orgBotDAL
});
const identityJwtAuthService = identityJwtAuthServiceFactory({
identityJwtAuthDAL,
permissionService,
identityAccessTokenDAL,
identityOrgMembershipDAL,
licenseService,
kmsService
});
const dynamicSecretProviders = buildDynamicSecretProviders();
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
queueService,
@ -1215,6 +1272,7 @@ export const registerRoutes = async (
auditLogDAL,
queueService,
secretVersionDAL,
secretDAL,
secretFolderVersionDAL: folderVersionDAL,
snapshotDAL,
identityAccessTokenDAL,
@ -1261,7 +1319,8 @@ export const registerRoutes = async (
const cmekService = cmekServiceFactory({
kmsDAL,
kmsService,
permissionService
permissionService,
projectDAL
});
const externalMigrationQueue = externalMigrationQueueFactory({
@ -1347,6 +1406,7 @@ export const registerRoutes = async (
identityAwsAuth: identityAwsAuthService,
identityAzureAuth: identityAzureAuthService,
identityOidcAuth: identityOidcAuthService,
identityJwtAuth: identityJwtAuthService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
secretApprovalPolicy: secretApprovalPolicyService,
@ -1360,6 +1420,8 @@ export const registerRoutes = async (
auditLog: auditLogService,
auditLogStream: auditLogStreamService,
certificate: certificateService,
sshCertificateAuthority: sshCertificateAuthorityService,
sshCertificateTemplate: sshCertificateTemplateService,
certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService,

View File

@ -220,6 +220,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
id: true,
name: true,
description: true,
type: true,
slug: true,
autoCapitalization: true,
orgId: true,

View File

@ -0,0 +1,386 @@
import { z } from "zod";
import { IdentityJwtAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { JWT_AUTH } 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 { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { JwtConfigurationType } from "@app/services/identity-jwt-auth/identity-jwt-auth-types";
import {
validateJwtAuthAudiencesField,
validateJwtBoundClaimsField
} from "@app/services/identity-jwt-auth/identity-jwt-auth-validators";
const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({
encryptedJwksCaCert: true,
encryptedPublicKeys: true
}).extend({
jwksCaCert: z.string(),
publicKeys: z.string().array()
});
const CreateBaseSchema = z.object({
boundIssuer: z.string().trim().default("").describe(JWT_AUTH.ATTACH.boundIssuer),
boundAudiences: validateJwtAuthAudiencesField.describe(JWT_AUTH.ATTACH.boundAudiences),
boundClaims: validateJwtBoundClaimsField.describe(JWT_AUTH.ATTACH.boundClaims),
boundSubject: z.string().trim().default("").describe(JWT_AUTH.ATTACH.boundSubject),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(JWT_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit)
});
const UpdateBaseSchema = z
.object({
boundIssuer: z.string().trim().default("").describe(JWT_AUTH.UPDATE.boundIssuer),
boundAudiences: validateJwtAuthAudiencesField.describe(JWT_AUTH.UPDATE.boundAudiences),
boundClaims: validateJwtBoundClaimsField.describe(JWT_AUTH.UPDATE.boundClaims),
boundSubject: z.string().trim().default("").describe(JWT_AUTH.UPDATE.boundSubject),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(JWT_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.UPDATE.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.UPDATE.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.UPDATE.accessTokenNumUsesLimit)
})
.partial();
const JwksConfigurationSchema = z.object({
configurationType: z.literal(JwtConfigurationType.JWKS).describe(JWT_AUTH.ATTACH.configurationType),
jwksUrl: z.string().trim().url().describe(JWT_AUTH.ATTACH.jwksUrl),
jwksCaCert: z.string().trim().default("").describe(JWT_AUTH.ATTACH.jwksCaCert),
publicKeys: z.string().array().optional().default([]).describe(JWT_AUTH.ATTACH.publicKeys)
});
const StaticConfigurationSchema = z.object({
configurationType: z.literal(JwtConfigurationType.STATIC).describe(JWT_AUTH.ATTACH.configurationType),
jwksUrl: z.string().trim().optional().default("").describe(JWT_AUTH.ATTACH.jwksUrl),
jwksCaCert: z.string().trim().optional().default("").describe(JWT_AUTH.ATTACH.jwksCaCert),
publicKeys: z.string().min(1).array().min(1).describe(JWT_AUTH.ATTACH.publicKeys)
});
export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/jwt-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
description: "Login with JWT Auth",
body: z.object({
identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId),
jwt: z.string().trim()
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityJwtAuth.login({
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_JWT_AUTH,
metadata: {
identityId: identityJwtAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityJwtAuthId: identityJwtAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityJwtAuth.accessTokenTTL,
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/jwt-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach JWT Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(JWT_AUTH.ATTACH.identityId)
}),
body: z.discriminatedUnion("configurationType", [
JwksConfigurationSchema.merge(CreateBaseSchema),
StaticConfigurationSchema.merge(CreateBaseSchema)
]),
response: {
200: z.object({
identityJwtAuth: IdentityJwtAuthResponseSchema
})
}
},
handler: async (req) => {
const identityJwtAuth = await server.services.identityJwtAuth.attachJwtAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityJwtAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_JWT_AUTH,
metadata: {
identityId: identityJwtAuth.identityId,
configurationType: identityJwtAuth.configurationType,
jwksUrl: identityJwtAuth.jwksUrl,
jwksCaCert: identityJwtAuth.jwksCaCert,
publicKeys: identityJwtAuth.publicKeys,
boundIssuer: identityJwtAuth.boundIssuer,
boundAudiences: identityJwtAuth.boundAudiences,
boundClaims: identityJwtAuth.boundClaims as Record<string, string>,
boundSubject: identityJwtAuth.boundSubject,
accessTokenTTL: identityJwtAuth.accessTokenTTL,
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityJwtAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit
}
}
});
return {
identityJwtAuth
};
}
});
server.route({
method: "PATCH",
url: "/jwt-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update JWT Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(JWT_AUTH.UPDATE.identityId)
}),
body: z.discriminatedUnion("configurationType", [
JwksConfigurationSchema.merge(UpdateBaseSchema),
StaticConfigurationSchema.merge(UpdateBaseSchema)
]),
response: {
200: z.object({
identityJwtAuth: IdentityJwtAuthResponseSchema
})
}
},
handler: async (req) => {
const identityJwtAuth = await server.services.identityJwtAuth.updateJwtAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityJwtAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_JWT_AUTH,
metadata: {
identityId: identityJwtAuth.identityId,
configurationType: identityJwtAuth.configurationType,
jwksUrl: identityJwtAuth.jwksUrl,
jwksCaCert: identityJwtAuth.jwksCaCert,
publicKeys: identityJwtAuth.publicKeys,
boundIssuer: identityJwtAuth.boundIssuer,
boundAudiences: identityJwtAuth.boundAudiences,
boundClaims: identityJwtAuth.boundClaims as Record<string, string>,
boundSubject: identityJwtAuth.boundSubject,
accessTokenTTL: identityJwtAuth.accessTokenTTL,
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityJwtAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit
}
}
});
return { identityJwtAuth };
}
});
server.route({
method: "GET",
url: "/jwt-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve JWT Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(JWT_AUTH.RETRIEVE.identityId)
}),
response: {
200: z.object({
identityJwtAuth: IdentityJwtAuthResponseSchema
})
}
},
handler: async (req) => {
const identityJwtAuth = await server.services.identityJwtAuth.getJwtAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityJwtAuth.orgId,
event: {
type: EventType.GET_IDENTITY_JWT_AUTH,
metadata: {
identityId: identityJwtAuth.identityId
}
}
});
return { identityJwtAuth };
}
});
server.route({
method: "DELETE",
url: "/jwt-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete JWT Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(JWT_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityJwtAuth: IdentityJwtAuthResponseSchema.omit({
publicKeys: true,
jwksCaCert: true
})
})
}
},
handler: async (req) => {
const identityJwtAuth = await server.services.identityJwtAuth.revokeJwtAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityJwtAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_JWT_AUTH,
metadata: {
identityId: identityJwtAuth.identityId
}
}
});
return { identityJwtAuth };
}
});
};

View File

@ -328,7 +328,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
project: SanitizedProjectSchema.pick({ name: true, id: true, type: true })
})
)
})

View File

@ -12,6 +12,7 @@ import { registerIdentityAccessTokenRouter } from "./identity-access-token-route
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router";
@ -54,6 +55,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityAwsAuthRouter);
await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter);
await authRouter.register(registerIdentityJwtAuthRouter);
},
{ prefix: "/auth" }
);

View File

@ -1185,4 +1185,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
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 };
}
});
};

View File

@ -16,6 +16,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
@ -29,9 +30,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
200: z.object({
organizations: OrganizationsSchema.extend({
orgAuthMethod: z.string()
}).array()
organizations: sanitizedOrganizationSchema
.extend({
orgAuthMethod: z.string()
})
.array()
})
}
},

View File

@ -5,6 +5,7 @@ import {
ProjectMembershipsSchema,
ProjectRolesSchema,
ProjectSlackConfigsSchema,
ProjectType,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
@ -135,7 +136,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
includeRoles: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.transform((value) => value === "true"),
type: z
.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, ProjectType.SSH, "all"])
.optional()
}),
response: {
200: z.object({
@ -154,7 +158,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
actorOrgId: req.permission.orgId,
type: req.query.type
});
return { workspaces };
}

View File

@ -5,10 +5,12 @@ import {
OrgMembershipsSchema,
ProjectMembershipsSchema,
ProjectsSchema,
ProjectType,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@ -78,6 +80,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
params: z.object({
organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId)
}),
querystring: z.object({
type: z.nativeEnum(ProjectType).optional().describe(ORGANIZATIONS.GET_PROJECTS.type)
}),
response: {
200: z.object({
workspaces: z
@ -104,7 +109,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
orgId: req.params.organizationId,
type: req.query.type
});
return { workspaces };
@ -281,7 +287,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
project: ProjectsSchema.pick({ name: true, id: true }),
project: ProjectsSchema.pick({ name: true, id: true, type: true }),
roles: z.array(
z.object({
id: z.string(),
@ -358,21 +364,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
organization: OrganizationsSchema
organization: OrganizationsSchema,
accessToken: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
handler: async (req, res) => {
if (req.auth.actor !== ActorType.USER) return;
const organization = await server.services.org.deleteOrganizationById(
req.permission.id,
req.params.organizationId,
req.permission.authMethod,
req.permission.orgId
);
return { organization };
const cfg = getConfig();
const { organization, tokens } = await server.services.org.deleteOrganizationById({
userId: req.permission.id,
orgId: req.params.organizationId,
actorAuthMethod: req.permission.authMethod,
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 };
}
});
};

View File

@ -5,10 +5,14 @@ import {
CertificatesSchema,
PkiAlertsSchema,
PkiCollectionsSchema,
ProjectKeysSchema
ProjectKeysSchema,
ProjectType
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
@ -159,7 +163,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
template: slugSchema({ field: "Template Name", max: 64 })
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template)
.describe(PROJECTS.CREATE.template),
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
}),
response: {
200: z.object({
@ -178,7 +183,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
workspaceDescription: req.body.projectDescription,
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId,
template: req.body.template
template: req.body.template,
type: req.body.type
});
await server.services.telemetry.sendPostHogEvents({
@ -497,4 +503,101 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
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 };
}
});
};

View File

@ -1,10 +1,11 @@
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 { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
@ -134,7 +135,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
description: "Return organizations that current user is part of",
response: {
200: z.object({
organizations: OrganizationsSchema.array()
organizations: sanitizedOrganizationSchema.array()
})
}
},

View File

@ -12,9 +12,12 @@ export type TTokenDALFactory = ReturnType<typeof tokenDALFactory>;
export const tokenDALFactory = (db: TDbClient) => {
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 {
const doc = await db.replicaNode()(TableName.AuthTokenSession).where(filter).first();
const doc = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter).first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindOneTokenSession" });
@ -54,10 +57,11 @@ export const tokenDALFactory = (db: TDbClient) => {
const insertTokenSession = async (
userId: string,
ip: string,
userAgent: string
userAgent: string,
tx?: Knex
): Promise<TAuthTokenSessions | undefined> => {
try {
const [session] = await db(TableName.AuthTokenSession)
const [session] = await (tx || db)(TableName.AuthTokenSession)
.insert({
userId,
ip,

View File

@ -1,6 +1,7 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { Knex } from "knex";
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
@ -123,14 +124,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
return deletedToken?.[0];
};
const getUserTokenSession = async ({
userId,
ip,
userAgent
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent });
const getUserTokenSession = async (
{ userId, ip, userAgent }: TIssueAuthTokenDTO,
tx?: Knex
): Promise<TAuthTokenSessions | undefined> => {
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent }, tx);
if (!session) {
session = await tokenDAL.insertTokenSession(userId, ip, userAgent);
session = await tokenDAL.insertTokenSession(userId, ip, userAgent, tx);
}
return session;
};

View File

@ -1,5 +1,6 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
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
* 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 isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
if (!isDeviceSeen) {
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) {
await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin,
@ -97,30 +98,36 @@ export const authLoginServiceFactory = ({
* 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
*/
const generateUserTokens = async ({
user,
ip,
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,
const generateUserTokens = async (
{
user,
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");
const accessToken = jwt.sign(

View File

@ -15,7 +15,7 @@ import {
/* eslint-disable no-bitwise */
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
return randomBytes.toString("hex");
};

View File

@ -5,7 +5,7 @@ import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
@ -77,7 +77,10 @@ type TCertificateAuthorityServiceFactoryDep = {
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
projectDAL: Pick<
TProjectDALFactory,
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
>;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@ -123,14 +126,24 @@ export const certificateAuthorityServiceFactory = ({
}: TCreateCaDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
let projectId = project.id;
const { permission } = await permissionService.getProjectPermission(
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -161,7 +174,7 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.create(
{
projectId: project.id,
projectId,
type,
organization,
ou,
@ -185,7 +198,7 @@ export const certificateAuthorityServiceFactory = ({
);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: project.id,
projectId,
projectDAL,
kmsService
});
@ -323,13 +336,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@ -348,13 +362,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
@ -434,13 +449,14 @@ export const certificateAuthorityServiceFactory = ({
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -819,13 +835,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -965,13 +982,14 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -1127,13 +1145,14 @@ export const certificateAuthorityServiceFactory = ({
throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
@ -1455,13 +1474,14 @@ export const certificateAuthorityServiceFactory = ({
}
if (!dto.isInternal) {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
dto.actor,
dto.actorId,
ca.projectId,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,

View File

@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
import { ProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -67,13 +67,14 @@ export const certificateTemplateServiceFactory = ({
message: `CA with ID ${caId} not found`
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -128,13 +129,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@ -185,13 +187,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
@ -252,13 +255,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@ -336,13 +340,14 @@ export const certificateTemplateServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,

View File

@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { ProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -49,13 +50,14 @@ export const certificateServiceFactory = ({
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
@ -72,13 +74,14 @@ export const certificateServiceFactory = ({
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
@ -106,13 +109,14 @@ export const certificateServiceFactory = ({
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);

View File

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -14,24 +15,33 @@ import {
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
type TCmekServiceFactoryDep = {
kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory;
permissionService: TPermissionServiceFactory;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
};
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission(
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, projectDAL }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId: preSplitProjectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
let projectId = preSplitProjectId;
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS);
if (cmekProjectFromSplit) {
projectId = cmekProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
const cmek = await kmsService.generateKmsKey({
@ -50,13 +60,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
@ -72,13 +83,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
@ -87,7 +99,16 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => {
const listCmeksByProjectId = async (
{ projectId: preSplitProjectId, ...filters }: TListCmeksByProjectIdDTO,
actor: OrgServiceActor
) => {
let projectId = preSplitProjectId;
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(preSplitProjectId, ProjectType.KMS);
if (cmekProjectFromSplit) {
projectId = cmekProjectFromSplit.id;
}
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
@ -112,7 +133,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
@ -120,6 +141,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek);
const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId });
@ -138,13 +160,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type,
actor.id,
key.projectId,
actor.authMethod,
actor.orgId
);
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek);

View File

@ -37,7 +37,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
)
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
@ -47,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
db.ref("name").withSchema(TableName.Identity)
)
.first();
@ -61,7 +62,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt
};
} catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });

View File

@ -171,7 +171,8 @@ export const identityAccessTokenServiceFactory = ({
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth
};
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];

View File

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityJwtAuthDALFactory = ReturnType<typeof identityJwtAuthDALFactory>;
export const identityJwtAuthDALFactory = (db: TDbClient) => {
const jwtAuthOrm = ormify(db, TableName.IdentityJwtAuth);
return jwtAuthOrm;
};

View File

@ -0,0 +1,13 @@
import picomatch from "picomatch";
export const doesFieldValueMatchJwtPolicy = (fieldValue: string | boolean | number, policyValue: string) => {
if (typeof fieldValue === "boolean") {
return fieldValue === (policyValue === "true");
}
if (typeof fieldValue === "number") {
return fieldValue === parseInt(policyValue, 10);
}
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
};

View File

@ -0,0 +1,534 @@
import { ForbiddenError } from "@casl/ability";
import https from "https";
import jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa";
import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
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 { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns";
import {
JwtConfigurationType,
TAttachJwtAuthDTO,
TGetJwtAuthDTO,
TLoginJwtAuthDTO,
TRevokeJwtAuthDTO,
TUpdateJwtAuthDTO
} from "./identity-jwt-auth-types";
type TIdentityJwtAuthServiceFactoryDep = {
identityJwtAuthDAL: TIdentityJwtAuthDALFactory;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TIdentityJwtAuthServiceFactory = ReturnType<typeof identityJwtAuthServiceFactory>;
export const identityJwtAuthServiceFactory = ({
identityJwtAuthDAL,
identityOrgMembershipDAL,
permissionService,
licenseService,
identityAccessTokenDAL,
kmsService
}: TIdentityJwtAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
if (!identityJwtAuth) {
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
identityId: identityJwtAuth.identityId
});
if (!identityMembershipOrg) {
throw new NotFoundError({
message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found`
});
}
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId
});
const decodedToken = jwt.decode(jwtValue, { complete: true });
if (!decodedToken) {
throw new UnauthorizedError({
message: "Invalid JWT"
});
}
let tokenData: Record<string, string | boolean | number> = {};
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
const decryptedJwksCaCert = orgDataKeyDecryptor({
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
}).toString();
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
const client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl,
requestAgent
});
const { kid } = decodedToken.header;
const jwtSigningKey = await client.getSigningKey(kid);
try {
tokenData = jwt.verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new UnauthorizedError({
message: `Access denied: ${error.message}`
});
}
throw error;
}
} else {
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
.toString()
.split(",");
const errors: string[] = [];
let isMatchAnyKey = false;
for (const publicKey of decryptedPublicKeys) {
try {
tokenData = jwt.verify(jwtValue, publicKey) as Record<string, string>;
isMatchAnyKey = true;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
errors.push(error.message);
}
}
}
if (!isMatchAnyKey) {
throw new UnauthorizedError({
message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}`
});
}
}
if (identityJwtAuth.boundIssuer) {
if (tokenData.iss !== identityJwtAuth.boundIssuer) {
throw new ForbiddenRequestError({
message: "Access denied: issuer mismatch"
});
}
}
if (identityJwtAuth.boundSubject) {
if (!tokenData.sub) {
throw new UnauthorizedError({
message: "Access denied: token has no subject field"
});
}
if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) {
throw new ForbiddenRequestError({
message: "Access denied: subject not allowed"
});
}
}
if (identityJwtAuth.boundAudiences) {
if (!tokenData.aud) {
throw new UnauthorizedError({
message: "Access denied: token has no audience field"
});
}
if (
!identityJwtAuth.boundAudiences
.split(", ")
.some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue))
) {
throw new UnauthorizedError({
message: "Access denied: token audience not allowed"
});
}
}
if (identityJwtAuth.boundClaims) {
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
if (!tokenData[claimKey]) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
// handle both single and multi-valued claims
if (
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry))
) {
throw new UnauthorizedError({
message: `Access denied: claim mismatch for field ${claimKey}`
});
}
});
}
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityJwtAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityJwtAuth.accessTokenTTL,
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.JWT_AUTH
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityJwtAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg };
};
const attachJwtAuth = async ({
identityId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachJwtAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
}
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
throw new BadRequestError({
message: "Failed to add JWT Auth to already configured identity"
});
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({
plainText: Buffer.from(jwksCaCert)
});
const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({
plainText: Buffer.from(publicKeys.join(","))
});
const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
const doc = await identityJwtAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
configurationType,
jwksUrl,
encryptedJwksCaCert,
encryptedPublicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
return doc;
});
return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys };
};
const updateJwtAuth = async ({
identityId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateJwtAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
throw new BadRequestError({
message: "Failed to update JWT Auth"
});
}
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityJwtAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updateQuery: TIdentityJwtAuthsUpdate = {
boundIssuer,
configurationType,
jwksUrl,
boundAudiences,
boundClaims,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
};
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
if (jwksCaCert !== undefined) {
const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({
plainText: Buffer.from(jwksCaCert)
});
updateQuery.encryptedJwksCaCert = encryptedJwksCaCert;
}
if (publicKeys) {
const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({
plainText: Buffer.from(publicKeys.join(","))
});
updateQuery.encryptedPublicKeys = encryptedPublicKeys;
}
const updatedJwtAuth = await identityJwtAuthDAL.updateById(identityJwtAuth.id, updateQuery);
const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedJwksCaCert }).toString();
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedPublicKeys })
.toString()
.split(",");
return {
...updatedJwtAuth,
orgId: identityMembershipOrg.orgId,
jwksCaCert: decryptedJwksCaCert,
publicKeys: decryptedPublicKeys
};
};
const getJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetJwtAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
throw new BadRequestError({
message: "The identity does not have JWT Auth attached"
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedJwksCaCert }).toString();
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
.toString()
.split(",");
return {
...identityJwtAuth,
orgId: identityMembershipOrg.orgId,
jwksCaCert: decryptedJwksCaCert,
publicKeys: decryptedPublicKeys
};
};
const revokeJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeJwtAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
throw new NotFoundError({ message: "Failed to find identity" });
}
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) {
throw new BadRequestError({
message: "The identity does not have JWT auth"
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
if (!isAtLeastAsPrivileged(permission, rolePermission)) {
throw new ForbiddenRequestError({
message: "Failed to revoke JWT auth of identity with more privileged role"
});
}
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx);
return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityJwtAuth;
};
return {
login,
attachJwtAuth,
updateJwtAuth,
getJwtAuth,
revokeJwtAuth
};
};

View File

@ -0,0 +1,51 @@
import { TProjectPermission } from "@app/lib/types";
export enum JwtConfigurationType {
JWKS = "jwks",
STATIC = "static"
}
export type TAttachJwtAuthDTO = {
identityId: string;
configurationType: JwtConfigurationType;
jwksUrl: string;
jwksCaCert: string;
publicKeys: string[];
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateJwtAuthDTO = {
identityId: string;
configurationType?: JwtConfigurationType;
jwksUrl?: string;
jwksCaCert?: string;
publicKeys?: string[];
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetJwtAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeJwtAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TLoginJwtAuthDTO = {
identityId: string;
jwt: string;
};

View File

@ -0,0 +1,25 @@
import { z } from "zod";
export const validateJwtAuthAudiencesField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
return data
.split(",")
.map((id) => id.trim())
.join(", ");
});
export const validateJwtBoundClaimsField = z.record(z.string()).transform((data) => {
const formattedClaims: Record<string, string> = {};
Object.keys(data).forEach((key) => {
formattedClaims[key] = data[key]
.split(",")
.map((id) => id.trim())
.join(", ");
});
return formattedClaims;
});

View File

@ -102,6 +102,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
@ -126,7 +127,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
createdAt,
updatedAt,
projectId,
projectName
projectName,
projectType
}) => ({
id,
identityId,
@ -147,7 +149,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
},
project: {
id: projectId,
name: projectName
name: projectName,
type: projectType
}
}),
key: "id",

View File

@ -7,7 +7,8 @@ export const buildAuthMethods = ({
kubernetesId,
oidcId,
azureId,
tokenId
tokenId,
jwtId
}: {
uaId?: string;
gcpId?: string;
@ -16,6 +17,7 @@ export const buildAuthMethods = ({
oidcId?: string;
azureId?: string;
tokenId?: string;
jwtId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
@ -24,6 +26,7 @@ export const buildAuthMethods = ({
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null]
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null]
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
};

View File

@ -6,6 +6,7 @@ import {
TIdentityAwsAuths,
TIdentityAzureAuths,
TIdentityGcpAuths,
TIdentityJwtAuths,
TIdentityKubernetesAuths,
TIdentityOidcAuths,
TIdentityOrgMemberships,
@ -70,6 +71,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
.leftJoin<TIdentityJwtAuths>(
TableName.IdentityJwtAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityJwtAuth}.identityId`
)
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
@ -81,6 +87,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
db.ref("name").withSchema(TableName.Identity)
);
@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId",
`${TableName.IdentityTokenAuth}.identityId`
)
.leftJoin<TIdentityJwtAuths>(
TableName.IdentityJwtAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityJwtAuth}.identityId`
)
.select(
db.ref("id").withSchema("paginatedIdentity"),
@ -200,7 +212,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@ -237,6 +250,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
uaId,
awsId,
gcpId,
jwtId,
kubernetesId,
oidcId,
azureId,
@ -271,7 +285,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
kubernetesId,
oidcId,
azureId,
tokenId
tokenId,
jwtId
})
}
}),

View File

@ -0,0 +1,5 @@
export type TCircleCIContext = {
id: string;
name: string;
created_at: string;
};

View File

@ -4,13 +4,21 @@ import { Octokit } from "@octokit/rest";
import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client";
import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import {
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
TIntegrationAuths,
TIntegrationAuthsInsert
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
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 { TIntegrationDALFactory } from "../integration/integration-dal";
@ -18,6 +26,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list";
import { TCircleCIContext } from "./integration-app-types";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
@ -25,6 +34,7 @@ import {
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
TCircleCIOrganization,
TDeleteIntegrationAuthByIdDTO,
TDeleteIntegrationAuthsDTO,
TDuplicateGithubIntegrationAuthDTO,
@ -36,6 +46,7 @@ import {
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthCircleCIOrganizationDTO,
TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO,
@ -145,13 +156,14 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const tokenExchange = await exchangeCode({ integration, code, url, installationId });
@ -254,13 +266,14 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const updateDoc: TIntegrationAuthsInsert = {
@ -1570,6 +1583,120 @@ export const integrationAuthServiceFactory = ({
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 ({
projectId,
integration,
@ -1782,6 +1909,7 @@ export const integrationAuthServiceFactory = ({
getTeamcityBuildConfigs,
getBitbucketWorkspaces,
getBitbucketEnvironments,
getCircleCIOrganizations,
getIntegrationAccessToken,
duplicateIntegrationAuth,
getOctopusDeploySpaces,

View File

@ -128,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthCircleCIOrganizationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TVercelBranches = {
ref: string;
lastCommit: string;
@ -189,6 +193,14 @@ export type TTeamCityBuildConfig = {
webUrl: string;
};
export type TCircleCIOrganization = {
id: string;
vcsType: string;
name: string;
avatarUrl: string;
slug: string;
};
export type TIntegrationsWithEnvironment = TIntegrations & {
environment?:
| {
@ -215,6 +227,11 @@ export enum OctopusDeployScope {
// add tenant, variable set, etc.
}
export enum CircleCiScope {
Project = "project",
Context = "context"
}
export type TOctopusDeployVariableSet = {
Id: string;
OwnerId: string;

View File

@ -76,7 +76,6 @@ export enum IntegrationUrls {
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
FLYIO_API_URL = "https://api.fly.io/graphql",
CIRCLECI_API_URL = "https://circleci.com/api",
DATABRICKS_API_URL = "https:/xxxx.com/api",
TRAVISCI_API_URL = "https://api.travis-ci.com",
SUPABASE_API_URL = "https://api.supabase.com",
LARAVELFORGE_API_URL = "https://forge.laravel.com",
@ -218,9 +217,9 @@ export const getIntegrationOptions = async () => {
docsLink: ""
},
{
name: "Circle CI",
name: "CircleCI",
slug: "circleci",
image: "Circle CI.png",
image: "CircleCI.png",
isAvailable: true,
type: "pat",
clientId: "",

View File

@ -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;
};

View File

@ -39,13 +39,19 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-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 {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
Integrations,
IntegrationUrls
} from "./integration-list";
import { isAzureKeyVaultReference } from "./integration-sync-secret-fns";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
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 azureAppConfigSecrets = (
await getCompleteAzureAppConfigValues(
`${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*`
)
).reduce(
const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
metadata.azureLabel ? `&label=${metadata.azureLabel}` : ""
}`;
const azureAppConfigSecrets = (await getCompleteAzureAppConfigValues(azureAppConfigValuesUrl)).reduce(
(accum, entry) => {
accum[entry.key] = entry.value;
@ -405,14 +412,24 @@ const syncSecretsAzureAppConfig = async ({
}
// create or update secrets on Azure App Config
for await (const key of Object.keys(secrets)) {
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
await request.put(
`${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: {
Authorization: `Bearer ${accessToken}`
},
@ -432,6 +449,11 @@ const syncSecretsAzureAppConfig = async ({
headers: {
Authorization: `Bearer ${accessToken}`
},
...(metadata.azureLabel && {
params: {
label: metadata.azureLabel
}
}),
// we force IPV4 because docker setup fails with ipv6
httpsAgent: new https.Agent({
family: 4
@ -2245,102 +2267,174 @@ const syncSecretsCircleCI = async ({
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const getProjectSlug = async () => {
const requestConfig = {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
};
try {
const projectDetails = (
await request.get<{ slug: string }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
requestConfig
if (integration.scope === CircleCiScope.Context) {
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.put(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`,
{
value: secrets[key].value
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
)
).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.");
}
}
}
// get secrets from CircleCI
const getSecretsRes = async () => {
type EnvVars = {
variable: string;
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
try {
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
let nextPageToken: string | null | undefined;
const envVars: EnvVars[] = [];
// 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
},
{
while (nextPageToken !== null) {
const res = await request.get<{
items: EnvVars[];
next_page_token: string | null;
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, {
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
)
)
);
"Accept-Encoding": "application/json"
},
params: nextPageToken
? new URLSearchParams({
"page-token": nextPageToken
})
: undefined
});
// get secrets from CircleCI
const getSecretsRes = (
await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
envVars.push(...res.data.items);
nextPageToken = res.data.next_page_token;
}
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: {
"Circle-Token": accessToken,
"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}`, {
try {
const projectDetails = (
await request.get<{ slug: string }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
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: {
"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"
}
});
}
})
);
}
};
/**

View File

@ -35,6 +35,8 @@ export const IntegrationMetadataSchema = z.object({
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
azureLabel: z.string().optional().describe(INTEGRATION.CREATE.metadata.azureLabel),
githubVisibility: z
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
.optional()

View File

@ -1,5 +1,6 @@
import { ForbiddenError, subject } 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 { NotFoundError } from "@app/lib/errors";
@ -80,13 +81,14 @@ export const integrationServiceFactory = ({
if (!integrationAuth)
throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan(
@ -158,13 +160,14 @@ export const integrationServiceFactory = ({
const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
integration.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const newEnvironment = environment || integration.environment.slug;
@ -293,13 +296,14 @@ export const integrationServiceFactory = ({
const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
integration.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);

View 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
});

View File

@ -15,7 +15,6 @@ import {
TProjectUserMembershipRolesInsert,
TUsers
} from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
@ -32,11 +31,13 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { TQueueServiceFactory } from "@app/queue";
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-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 { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
@ -48,6 +49,10 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-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 { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@ -70,6 +75,9 @@ import {
type TOrgServiceFactoryDep = {
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
orgDAL: TOrgDALFactory;
orgBotDAL: TOrgBotDALFactory;
orgRoleDAL: TOrgRoleDALFactory;
@ -98,6 +106,8 @@ type TOrgServiceFactoryDep = {
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
};
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@ -105,6 +115,9 @@ export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
export const orgServiceFactory = ({
userAliasDAL,
orgDAL,
secretDAL,
secretV2BridgeDAL,
folderDAL,
userDAL,
groupDAL,
orgRoleDAL,
@ -125,7 +138,9 @@ export const orgServiceFactory = ({
projectBotDAL,
projectUserMembershipRoleDAL,
identityMetadataDAL,
projectBotService
projectBotService,
queueService,
loginService
}: TOrgServiceFactoryDep) => {
/*
* Get organization details by the organization id
@ -196,26 +211,18 @@ export const orgServiceFactory = ({
return org;
};
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
let workspaces: (TProjects & { organization: string } & {
environments: {
id: string;
slug: string;
name: string;
}[];
})[];
const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => {
if (actor === ActorType.USER) {
workspaces = await projectDAL.findAllProjects(actorId);
} else if (actor === ActorType.IDENTITY) {
workspaces = await projectDAL.findAllProjectsByIdentity(actorId);
} else {
throw new BadRequestError({ message: "Invalid actor type" });
const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all");
return workspaces;
}
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
if (actor === ActorType.IDENTITY) {
const workspaces = await projectDAL.findAllProjectsByIdentity(actorId, type);
return workspaces;
}
throw new BadRequestError({ message: "Invalid actor type" });
};
const addGhostUser = async (orgId: string, tx?: Knex) => {
@ -428,24 +435,88 @@ export const orgServiceFactory = ({
/*
* Delete organization by id
* */
const deleteOrganizationById = async (
userId: string,
orgId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const deleteOrganizationById = async ({
userId,
authorizationHeader,
userAgentHeader,
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);
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin)
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) {
throw new ForbiddenRequestError({
name: "DeleteOrganizationById",
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

View File

@ -1,3 +1,4 @@
import { ProjectType } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type";
@ -55,6 +56,7 @@ export type TFindAllWorkspacesDTO = {
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
orgId: string;
type?: ProjectType;
};
export type TUpdateOrgDTO = {

View File

@ -1,5 +1,6 @@
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 { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@ -8,6 +9,7 @@ import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-colle
import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TPkiAlertDALFactory } from "./pki-alert-dal";
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
@ -19,6 +21,7 @@ type TPkiAlertServiceFactoryDep = {
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: Pick<TSmtpService, "sendMail">;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
};
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
@ -27,7 +30,8 @@ export const pkiAlertServiceFactory = ({
pkiAlertDAL,
pkiCollectionDAL,
permissionService,
smtpService
smtpService,
projectDAL
}: TPkiAlertServiceFactoryDep) => {
const sendPkiItemExpiryNotices = async () => {
const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting();
@ -63,7 +67,7 @@ export const pkiAlertServiceFactory = ({
};
const createPkiAlert = async ({
projectId,
projectId: preSplitProjectId,
name,
pkiCollectionId,
alertBeforeDays,
@ -73,13 +77,23 @@ export const pkiAlertServiceFactory = ({
actor,
actorOrgId
}: TCreateAlertDTO) => {
const { permission } = await permissionService.getProjectPermission(
let projectId = preSplitProjectId;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
@ -128,13 +142,14 @@ export const pkiAlertServiceFactory = ({
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
alert.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
@ -160,13 +175,14 @@ export const pkiAlertServiceFactory = ({
let alert = await pkiAlertDAL.findById(alertId);
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
alert.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
alert = await pkiAlertDAL.deleteById(alertId);

View File

@ -1,12 +1,13 @@
import { ForbiddenError } from "@casl/ability";
import { TPkiCollectionItems } from "@app/db/schemas";
import { ProjectType, TPkiCollectionItems } 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 { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TPkiCollectionDALFactory } from "./pki-collection-dal";
import { transformPkiCollectionItem } from "./pki-collection-fns";
import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal";
@ -30,6 +31,7 @@ type TPkiCollectionServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
};
export type TPkiCollectionServiceFactory = ReturnType<typeof pkiCollectionServiceFactory>;
@ -39,24 +41,35 @@ export const pkiCollectionServiceFactory = ({
pkiCollectionItemDAL,
certificateAuthorityDAL,
certificateDAL,
permissionService
permissionService,
projectDAL
}: TPkiCollectionServiceFactoryDep) => {
const createPkiCollection = async ({
name,
description,
projectId,
projectId: preSplitProjectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreatePkiCollectionDTO) => {
const { permission } = await permissionService.getProjectPermission(
let projectId = preSplitProjectId;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -106,13 +119,14 @@ export const pkiCollectionServiceFactory = ({
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
@ -133,13 +147,14 @@ export const pkiCollectionServiceFactory = ({
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
@ -205,13 +220,14 @@ export const pkiCollectionServiceFactory = ({
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
@ -298,13 +314,14 @@ export const pkiCollectionServiceFactory = ({
if (!pkiCollectionItem) throw new NotFoundError({ message: `PKI collection item with ID '${itemId}' not found` });
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
pkiCollection.projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,

View File

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@ -41,13 +42,14 @@ export const projectEnvServiceFactory = ({
name,
slug
}: TCreateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
const lock = await keyStore
@ -129,13 +131,14 @@ export const projectEnvServiceFactory = ({
id,
position
}: TUpdateEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
const lock = await keyStore
@ -192,13 +195,14 @@ export const projectEnvServiceFactory = ({
};
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
const { permission } = await permissionService.getProjectPermission(
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
const lock = await keyStore

View File

@ -217,20 +217,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("projectId").withSchema(TableName.Project)
db.ref("id").as("projectId").withSchema(TableName.Project),
db.ref("type").as("projectType").withSchema(TableName.Project)
)
.where({ isGhost: false });
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({
parentMapper: ({
email,
firstName,
username,
lastName,
publicKey,
isGhost,
id,
projectId,
projectName,
projectType
}) => ({
id,
userId,
projectId,
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
project: {
id: projectId,
name: projectName
name: projectName,
type: projectType
}
}),
key: "id",

View File

@ -1,7 +1,14 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas";
import {
ProjectsSchema,
ProjectType,
ProjectUpgradeStatus,
ProjectVersion,
TableName,
TProjectsUpdate
} from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
export const projectDALFactory = (db: TDbClient) => {
const projectOrm = ormify(db, TableName.Project);
const findAllProjects = async (userId: string) => {
const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
try {
const workspaces = await db
.replicaNode()(TableName.ProjectMembership)
.where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.orgId`, orgId)
.andWhere((qb) => {
if (projectType !== "all") {
void qb.where(`${TableName.Project}.type`, projectType);
}
})
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
selectAllTableCols(TableName.Project),
@ -31,14 +44,17 @@ export const projectDALFactory = (db: TDbClient) => {
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
const groups: string[] = await db(TableName.UserGroupMembership)
.where({ userId })
.select(selectAllTableCols(TableName.UserGroupMembership))
.pluck("groupId");
const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId");
const groupWorkspaces = await db(TableName.GroupProjectMembership)
.whereIn("groupId", groups)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.orgId`, orgId)
.andWhere((qb) => {
if (projectType !== "all") {
void qb.where(`${TableName.Project}.type`, projectType);
}
})
.whereNotIn(
`${TableName.Project}.id`,
workspaces.map(({ id }) => id)
@ -108,12 +124,17 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findAllProjectsByIdentity = async (identityId: string) => {
const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => {
try {
const workspaces = await db
.replicaNode()(TableName.IdentityProjectMembership)
.where({ identityId })
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
.andWhere((qb) => {
if (projectType) {
void qb.where(`${TableName.Project}.type`, projectType);
}
})
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
selectAllTableCols(TableName.Project),
@ -315,6 +336,22 @@ export const projectDALFactory = (db: TDbClient) => {
};
};
const getProjectFromSplitId = async (projectId: string, projectType: ProjectType) => {
try {
const project = await db(TableName.ProjectSplitBackfillIds)
.where({
sourceProjectId: projectId,
destinationProjectType: projectType
})
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectSplitBackfillIds}.destinationProjectId`)
.select(selectAllTableCols(TableName.Project))
.first();
return project;
} catch (error) {
throw new DatabaseError({ error, name: `Failed to find split project with id ${projectId}` });
}
};
return {
...projectOrm,
findAllProjects,
@ -325,6 +362,7 @@ export const projectDALFactory = (db: TDbClient) => {
findProjectByFilter,
findProjectBySlug,
findProjectWithOrg,
checkProjectUpgradeStatus
checkProjectUpgradeStatus,
getProjectFromSplitId
};
};

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