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-dev \
freetds-bin \ freetds-bin \
tdsodbc \ tdsodbc \
openssh \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Configure ODBC in production # Configure ODBC in production

View File

@ -139,7 +139,8 @@ RUN apk --update add \
freetds-dev \ freetds-dev \
bash \ bash \
curl \ curl \
git git \
openssh
# Configure ODBC in production # Configure ODBC in production
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini

View File

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

View File

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

View File

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

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

View File

@ -98,6 +98,9 @@ import {
TIdentityGcpAuths, TIdentityGcpAuths,
TIdentityGcpAuthsInsert, TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate, TIdentityGcpAuthsUpdate,
TIdentityJwtAuths,
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate,
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate, TIdentityKubernetesAuthsUpdate,
@ -199,6 +202,9 @@ import {
TProjectSlackConfigs, TProjectSlackConfigs,
TProjectSlackConfigsInsert, TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate, TProjectSlackConfigsUpdate,
TProjectSplitBackfillIds,
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate,
TProjectsUpdate, TProjectsUpdate,
TProjectTemplates, TProjectTemplates,
TProjectTemplatesInsert, TProjectTemplatesInsert,
@ -311,6 +317,21 @@ import {
TSlackIntegrations, TSlackIntegrations,
TSlackIntegrationsInsert, TSlackIntegrationsInsert,
TSlackIntegrationsUpdate, TSlackIntegrationsUpdate,
TSshCertificateAuthorities,
TSshCertificateAuthoritiesInsert,
TSshCertificateAuthoritiesUpdate,
TSshCertificateAuthoritySecrets,
TSshCertificateAuthoritySecretsInsert,
TSshCertificateAuthoritySecretsUpdate,
TSshCertificateBodies,
TSshCertificateBodiesInsert,
TSshCertificateBodiesUpdate,
TSshCertificates,
TSshCertificatesInsert,
TSshCertificatesUpdate,
TSshCertificateTemplates,
TSshCertificateTemplatesInsert,
TSshCertificateTemplatesUpdate,
TSuperAdmin, TSuperAdmin,
TSuperAdminInsert, TSuperAdminInsert,
TSuperAdminUpdate, TSuperAdminUpdate,
@ -372,6 +393,31 @@ declare module "knex/types/tables" {
interface Tables { interface Tables {
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>; [TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>; [TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
TSshCertificateAuthorities,
TSshCertificateAuthoritiesInsert,
TSshCertificateAuthoritiesUpdate
>;
[TableName.SshCertificateAuthoritySecret]: KnexOriginal.CompositeTableType<
TSshCertificateAuthoritySecrets,
TSshCertificateAuthoritySecretsInsert,
TSshCertificateAuthoritySecretsUpdate
>;
[TableName.SshCertificateTemplate]: KnexOriginal.CompositeTableType<
TSshCertificateTemplates,
TSshCertificateTemplatesInsert,
TSshCertificateTemplatesUpdate
>;
[TableName.SshCertificate]: KnexOriginal.CompositeTableType<
TSshCertificates,
TSshCertificatesInsert,
TSshCertificatesUpdate
>;
[TableName.SshCertificateBody]: KnexOriginal.CompositeTableType<
TSshCertificateBodies,
TSshCertificateBodiesInsert,
TSshCertificateBodiesUpdate
>;
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType< [TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
TCertificateAuthorities, TCertificateAuthorities,
TCertificateAuthoritiesInsert, TCertificateAuthoritiesInsert,
@ -590,6 +636,11 @@ declare module "knex/types/tables" {
TIdentityOidcAuthsInsert, TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate TIdentityOidcAuthsUpdate
>; >;
[TableName.IdentityJwtAuth]: KnexOriginal.CompositeTableType<
TIdentityJwtAuths,
TIdentityJwtAuthsInsert,
TIdentityJwtAuthsUpdate
>;
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
TIdentityUaClientSecrets, TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert, TIdentityUaClientSecretsInsert,
@ -830,5 +881,10 @@ declare module "knex/types/tables" {
TProjectTemplatesUpdate TProjectTemplatesUpdate
>; >;
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>; [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-aws-auths";
export * from "./identity-azure-auths"; export * from "./identity-azure-auths";
export * from "./identity-gcp-auths"; export * from "./identity-gcp-auths";
export * from "./identity-jwt-auths";
export * from "./identity-kubernetes-auths"; export * from "./identity-kubernetes-auths";
export * from "./identity-metadata"; export * from "./identity-metadata";
export * from "./identity-oidc-auths"; export * from "./identity-oidc-auths";
@ -64,6 +65,7 @@ export * from "./project-keys";
export * from "./project-memberships"; export * from "./project-memberships";
export * from "./project-roles"; export * from "./project-roles";
export * from "./project-slack-configs"; export * from "./project-slack-configs";
export * from "./project-split-backfill-ids";
export * from "./project-templates"; export * from "./project-templates";
export * from "./project-user-additional-privilege"; export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles"; export * from "./project-user-membership-roles";
@ -105,6 +107,11 @@ export * from "./secrets";
export * from "./secrets-v2"; export * from "./secrets-v2";
export * from "./service-tokens"; export * from "./service-tokens";
export * from "./slack-integrations"; export * from "./slack-integrations";
export * from "./ssh-certificate-authorities";
export * from "./ssh-certificate-authority-secrets";
export * from "./ssh-certificate-bodies";
export * from "./ssh-certificate-templates";
export * from "./ssh-certificates";
export * from "./super-admin"; export * from "./super-admin";
export * from "./totp-configs"; export * from "./totp-configs";
export * from "./trusted-ips"; export * from "./trusted-ips";

View File

@ -2,6 +2,11 @@ import { z } from "zod";
export enum TableName { export enum TableName {
Users = "users", Users = "users",
SshCertificateAuthority = "ssh_certificate_authorities",
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
SshCertificateTemplate = "ssh_certificate_templates",
SshCertificate = "ssh_certificates",
SshCertificateBody = "ssh_certificate_bodies",
CertificateAuthority = "certificate_authorities", CertificateAuthority = "certificate_authorities",
CertificateTemplateEstConfig = "certificate_template_est_configs", CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs", CertificateAuthorityCert = "certificate_authority_certs",
@ -68,6 +73,7 @@ export enum TableName {
IdentityUaClientSecret = "identity_ua_client_secrets", IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths", IdentityAwsAuth = "identity_aws_auths",
IdentityOidcAuth = "identity_oidc_auths", IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths",
IdentityOrgMembership = "identity_org_memberships", IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectMembershipRole = "identity_project_membership_role",
@ -105,6 +111,7 @@ export enum TableName {
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2", SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2", SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
SnapshotSecretV2 = "secret_snapshot_secrets_v2", SnapshotSecretV2 = "secret_snapshot_secrets_v2",
ProjectSplitBackfillIds = "project_split_backfill_ids",
// junction tables with tags // junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction", SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction", JnSecretTag = "secret_tag_junction",
@ -196,5 +203,13 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth", GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth", AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-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(), auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.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>; 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 { 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"; import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [ export const DEFAULT_PROJECT_ENVS = [
@ -24,6 +24,7 @@ export async function seed(knex: Knex): Promise<void> {
name: seedData1.project.name, name: seedData1.project.name,
orgId: seedData1.organization.id, orgId: seedData1.organization.id,
slug: "first-project", slug: "first-project",
type: ProjectType.SecretManager,
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
id: seedData1.project.id id: seedData1.project.id

View File

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

View File

@ -25,6 +25,9 @@ import { registerSecretRotationRouter } from "./secret-rotation-router";
import { registerSecretScanningRouter } from "./secret-scanning-router"; import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSecretVersionRouter } from "./secret-version-router"; import { registerSecretVersionRouter } from "./secret-version-router";
import { registerSnapshotRouter } from "./snapshot-router"; import { registerSnapshotRouter } from "./snapshot-router";
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
import { registerSshCertRouter } from "./ssh-certificate-router";
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerTrustedIpRouter } from "./trusted-ip-router";
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
@ -68,6 +71,15 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/pki" } { prefix: "/pki" }
); );
await server.register(
async (sshRouter) => {
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
},
{ prefix: "/ssh" }
);
await server.register( await server.register(
async (ssoRouter) => { async (ssoRouter) => {
await ssoRouter.register(registerSamlRouter); await ssoRouter.register(registerSamlRouter);

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

View File

@ -2,9 +2,12 @@ import {
TCreateProjectTemplateDTO, TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types"; } from "@app/ee/services/project-template/project-template-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher"; import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
@ -95,6 +98,11 @@ export enum EventType {
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth", UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth", GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
REVOKE_IDENTITY_OIDC_AUTH = "revoke-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", CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-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_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed", SECRET_APPROVAL_CLOSED = "secret-approval-closed",
SECRET_APPROVAL_REOPENED = "secret-approval-reopened", SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
SIGN_SSH_KEY = "sign-ssh-key",
ISSUE_SSH_CREDS = "issue-ssh-creds",
CREATE_SSH_CA = "create-ssh-certificate-authority",
GET_SSH_CA = "get-ssh-certificate-authority",
UPDATE_SSH_CA = "update-ssh-certificate-authority",
DELETE_SSH_CA = "delete-ssh-certificate-authority",
GET_SSH_CA_CERTIFICATE_TEMPLATES = "get-ssh-certificate-authority-certificate-templates",
CREATE_SSH_CERTIFICATE_TEMPLATE = "create-ssh-certificate-template",
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
CREATE_CA = "create-certificate-authority", CREATE_CA = "create-certificate-authority",
GET_CA = "get-certificate-authority", GET_CA = "get-certificate-authority",
UPDATE_CA = "update-certificate-authority", UPDATE_CA = "update-certificate-authority",
@ -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 { interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT; type: EventType.CREATE_ENVIRONMENT;
metadata: { 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 { interface CreateCa {
type: EventType.CREATE_CA; type: EventType.CREATE_CA;
metadata: { metadata: {
@ -1742,6 +1933,11 @@ export type Event =
| DeleteIdentityOidcAuthEvent | DeleteIdentityOidcAuthEvent
| UpdateIdentityOidcAuthEvent | UpdateIdentityOidcAuthEvent
| GetIdentityOidcAuthEvent | GetIdentityOidcAuthEvent
| LoginIdentityJwtAuthEvent
| AddIdentityJwtAuthEvent
| UpdateIdentityJwtAuthEvent
| GetIdentityJwtAuthEvent
| DeleteIdentityJwtAuthEvent
| CreateEnvironmentEvent | CreateEnvironmentEvent
| GetEnvironmentEvent | GetEnvironmentEvent
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
@ -1766,6 +1962,17 @@ export type Event =
| SecretApprovalClosed | SecretApprovalClosed
| SecretApprovalRequest | SecretApprovalRequest
| SecretApprovalReopened | SecretApprovalReopened
| SignSshKey
| IssueSshCreds
| CreateSshCa
| GetSshCa
| UpdateSshCa
| DeleteSshCa
| GetSshCaCertificateTemplates
| CreateSshCertificateTemplate
| UpdateSshCertificateTemplate
| GetSshCertificateTemplate
| DeleteSshCertificateTemplate
| CreateCa | CreateCa
| GetCa | GetCa
| UpdateCa | UpdateCa

View File

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

View File

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; 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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
@ -73,13 +73,14 @@ export const dynamicSecretServiceFactory = ({
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.CreateRootCredential, ProjectPermissionDynamicSecretActions.CreateRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -144,13 +145,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.EditRootCredential, ProjectPermissionDynamicSecretActions.EditRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
@ -227,13 +229,14 @@ export const dynamicSecretServiceFactory = ({
const projectId = project.id; const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionDynamicSecretActions.DeleteRootCredential, ProjectPermissionDynamicSecretActions.DeleteRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) 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("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId") db.ref("id").withSchema(TableName.Project).as("projectId")
); );
@ -284,13 +285,15 @@ export const permissionDALFactory = (db: TDbClient) => {
membershipCreatedAt, membershipCreatedAt,
groupMembershipCreatedAt, groupMembershipCreatedAt,
groupMembershipUpdatedAt, groupMembershipUpdatedAt,
membershipUpdatedAt membershipUpdatedAt,
projectType
}) => ({ }) => ({
orgId, orgId,
orgAuthEnforced, orgAuthEnforced,
userId, userId,
projectId, projectId,
username, username,
projectType,
id: membershipId || groupMembershipId, id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt, createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
@ -449,6 +452,7 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"), 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("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("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
@ -480,7 +484,14 @@ export const permissionDALFactory = (db: TDbClient) => {
const permission = sqlNestRelationships({ const permission = sqlNestRelationships({
data: docs, data: docs,
key: "membershipId", key: "membershipId",
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({ parentMapper: ({
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
orgId,
identityName,
projectType
}) => ({
id: membershipId, id: membershipId,
identityId, identityId,
username: identityName, username: identityName,
@ -488,6 +499,7 @@ export const permissionDALFactory = (db: TDbClient) => {
createdAt: membershipCreatedAt, createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt, updatedAt: membershipUpdatedAt,
orgId, orgId,
projectType,
// just a prefilled value // just a prefilled value
orgAuthEnforced: false orgAuthEnforced: false
}), }),

View File

@ -6,6 +6,7 @@ import handlebars from "handlebars";
import { import {
OrgMembershipRole, OrgMembershipRole,
ProjectMembershipRole, ProjectMembershipRole,
ProjectType,
ServiceTokenScopes, ServiceTokenScopes,
TIdentityProjectMemberships, TIdentityProjectMemberships,
TProjectMemberships TProjectMemberships
@ -255,6 +256,13 @@ export const permissionServiceFactory = ({
return { return {
permission, permission,
membership: userProjectPermission, 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) => hasRole: (role: string) =>
userProjectPermission.roles.findIndex( userProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@ -323,6 +331,13 @@ export const permissionServiceFactory = ({
return { return {
permission, permission,
membership: identityProjectPermission, 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) => hasRole: (role: string) =>
identityProjectPermission.roles.findIndex( identityProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
@ -361,7 +376,14 @@ export const permissionServiceFactory = ({
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return { return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), 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>; permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: undefined; membership: undefined;
hasRole: (arg: string) => boolean; hasRole: (arg: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
} // service token doesn't have both membership and roles } // service token doesn't have both membership and roles
: { : {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>; permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
@ -379,6 +402,7 @@ export const permissionServiceFactory = ({
roles: Array<{ role: string }>; roles: Array<{ role: string }>;
}; };
hasRole: (role: string) => boolean; hasRole: (role: string) => boolean;
ForbidOnInvalidProjectType: (type: ProjectType) => void;
}; };
const getProjectPermission = async <T extends ActorType>( const getProjectPermission = async <T extends ActorType>(

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; 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 { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors"; import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; 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` }); if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` });
const shouldUseBridge = snapshot.projectVersion === 3; const shouldUseBridge = snapshot.projectVersion === 3;
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
snapshot.projectId, snapshot.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback 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; } 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 = { export const ORGANIZATIONS = {
LIST_USER_MEMBERSHIPS: { LIST_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to get memberships from." 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." search: "The text string that identity membership names will be filtered by."
}, },
GET_PROJECTS: { 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: { LIST_GROUPS: {
organizationId: "The ID of the organization to list groups for." organizationId: "The ID of the organization to list groups for."
@ -445,6 +492,17 @@ export const PROJECTS = {
LIST_INTEGRATION_AUTHORIZATION: { LIST_INTEGRATION_AUTHORIZATION: {
workspaceId: "The ID of the project to list integration auths for." workspaceId: "The ID of the project to list integration auths for."
}, },
LIST_SSH_CAS: {
projectId: "The ID of the project to list SSH CAs for."
},
LIST_SSH_CERTIFICATES: {
projectId: "The ID of the project to list SSH certificates for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
limit: "The number of SSH certificates to return."
},
LIST_SSH_CERTIFICATE_TEMPLATES: {
projectId: "The ID of the project to list SSH certificate templates for."
},
LIST_CAS: { LIST_CAS: {
slug: "The slug of the project to list CAs for.", slug: "The slug of the project to list CAs for.",
status: "The status of the CA to filter by.", status: "The status of the CA to filter by.",
@ -1079,6 +1137,7 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.", secretAWSTag: "The tags for AWS secrets.",
azureLabel: "Define which label to assign to secrets created in Azure App Configuration.",
githubVisibility: githubVisibility:
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.", "Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
githubVisibilityRepoIds: githubVisibilityRepoIds:
@ -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 = { export const CERTIFICATE_AUTHORITIES = {
CREATE: { CREATE: {
projectSlug: "Slug of the project to create the CA in.", projectSlug: "Slug of the project to create the CA in.",

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

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

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

View File

@ -220,6 +220,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
id: true, id: true,
name: true, name: true,
description: true, description: true,
type: true,
slug: true, slug: true,
autoCapitalization: true, autoCapitalization: true,
orgId: 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({ identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string()) 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 { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
@ -54,6 +55,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAwsAuthRouter);
await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter);
await authRouter.register(registerIdentityJwtAuthRouter);
}, },
{ prefix: "/auth" } { prefix: "/auth" }
); );

View File

@ -1185,4 +1185,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
return { spaces }; return { spaces };
} }
}); });
server.route({
method: "GET",
url: "/:integrationAuthId/circleci/organizations",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
response: {
200: z.object({
organizations: z
.object({
name: z.string(),
slug: z.string(),
projects: z
.object({
name: z.string(),
id: z.string()
})
.array(),
contexts: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
.array()
})
}
},
handler: async (req) => {
const organizations = await server.services.integrationAuth.getCircleCIOrganizations({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
return { organizations };
}
});
}; };

View File

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

View File

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

View File

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

View File

@ -5,10 +5,14 @@ import {
CertificatesSchema, CertificatesSchema,
PkiAlertsSchema, PkiAlertsSchema,
PkiCollectionsSchema, PkiCollectionsSchema,
ProjectKeysSchema ProjectKeysSchema,
ProjectType
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema";
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
@ -159,7 +163,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
template: slugSchema({ field: "Template Name", max: 64 }) template: slugSchema({ field: "Template Name", max: 64 })
.optional() .optional()
.default(InfisicalProjectTemplate.Default) .default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template) .describe(PROJECTS.CREATE.template),
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@ -178,7 +183,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
workspaceDescription: req.body.projectDescription, workspaceDescription: req.body.projectDescription,
slug: req.body.slug, slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId, kmsKeyId: req.body.kmsKeyId,
template: req.body.template template: req.body.template,
type: req.body.type
}); });
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
@ -497,4 +503,101 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return { certificateTemplates }; return { certificateTemplates };
} }
}); });
server.route({
method: "GET",
url: "/:projectId/ssh-certificates",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId)
}),
querystring: z.object({
offset: z.coerce.number().default(0).describe(PROJECTS.LIST_SSH_CERTIFICATES.offset),
limit: z.coerce.number().default(25).describe(PROJECTS.LIST_SSH_CERTIFICATES.limit)
}),
response: {
200: z.object({
certificates: z.array(sanitizedSshCertificate),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { certificates, totalCount } = await server.services.project.listProjectSshCertificates({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
projectId: req.params.projectId,
offset: req.query.offset,
limit: req.query.limit
});
return { certificates, totalCount };
}
});
server.route({
method: "GET",
url: "/:projectId/ssh-certificate-templates",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CERTIFICATE_TEMPLATES.projectId)
}),
response: {
200: z.object({
certificateTemplates: z.array(sanitizedSshCertificateTemplate)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { certificateTemplates } = await server.services.project.listProjectSshCertificateTemplates({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
projectId: req.params.projectId
});
return { certificateTemplates };
}
});
server.route({
method: "GET",
url: "/:projectId/ssh-cas",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_CAS.projectId)
}),
response: {
200: z.object({
cas: z.array(sanitizedSshCa)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const cas = await server.services.project.listProjectSshCas({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
projectId: req.params.projectId
});
return { cas };
}
});
}; };

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { TUsers, UserDeviceSchema } from "@app/db/schemas"; import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
@ -50,13 +51,13 @@ export const authLoginServiceFactory = ({
* Not exported. This is to update user device list * Not exported. This is to update user device list
* If new device is found. Will be saved and a mail will be send * If new device is found. Will be saved and a mail will be send
*/ */
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => { const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string, tx?: Knex) => {
const devices = await UserDeviceSchema.parseAsync(user.devices || []); const devices = await UserDeviceSchema.parseAsync(user.devices || []);
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent); const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
if (!isDeviceSeen) { if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]); const newDeviceList = devices.concat([{ ip, userAgent }]);
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }); await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
if (user.email) { if (user.email) {
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin, template: SmtpTemplates.NewDeviceJoin,
@ -97,30 +98,36 @@ export const authLoginServiceFactory = ({
* Check user device and send mail if new device * Check user device and send mail if new device
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled * generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
*/ */
const generateUserTokens = async ({ const generateUserTokens = async (
user, {
ip, user,
userAgent,
organizationId,
authMethod,
isMfaVerified,
mfaMethod
}: {
user: TUsers;
ip: string;
userAgent: string;
organizationId?: string;
authMethod: AuthMethod;
isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
}) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent);
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip, ip,
userId: user.id userAgent,
}); organizationId,
authMethod,
isMfaVerified,
mfaMethod
}: {
user: TUsers;
ip: string;
userAgent: string;
organizationId?: string;
authMethod: AuthMethod;
isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
},
tx?: Knex
) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent, tx);
const tokenSession = await tokenService.getUserTokenSession(
{
userAgent,
ip,
userId: user.id
},
tx
);
if (!tokenSession) throw new Error("Failed to create token"); if (!tokenSession) throw new Error("Failed to create token");
const accessToken = jwt.sign( const accessToken = jwt.sign(

View File

@ -15,7 +15,7 @@ import {
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
export const createSerialNumber = () => { export const createSerialNumber = () => {
const randomBytes = crypto.randomBytes(20); const randomBytes = crypto.randomBytes(20); // 20 bytes = 160 bits
randomBytes[0] &= 0x7f; // ensure the first bit is 0 randomBytes[0] &= 0x7f; // ensure the first bit is 0
return randomBytes.toString("hex"); return randomBytes.toString("hex");
}; };

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -14,24 +15,33 @@ import {
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
type TCmekServiceFactoryDep = { type TCmekServiceFactoryDep = {
kmsService: TKmsServiceFactory; kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory; kmsDAL: TKmsKeyDALFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
}; };
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>; export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => { export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, projectDAL }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => { const createCmek = async ({ projectId: preSplitProjectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission( 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.type,
actor.id, actor.id,
projectId, projectId,
actor.authMethod, actor.authMethod,
actor.orgId actor.orgId
); );
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
const cmek = await kmsService.generateKmsKey({ 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" }); 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.type,
actor.id, actor.id,
key.projectId, key.projectId,
actor.authMethod, actor.authMethod,
actor.orgId actor.orgId
); );
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek); 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" }); 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.type,
actor.id, actor.id,
key.projectId, key.projectId,
actor.authMethod, actor.authMethod,
actor.orgId actor.orgId
); );
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
@ -87,7 +99,16 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek; 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( const { permission } = await permissionService.getProjectPermission(
actor.type, actor.type,
actor.id, actor.id,
@ -112,7 +133,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type, actor.type,
actor.id, actor.id,
key.projectId, key.projectId,
@ -120,6 +141,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
actor.orgId actor.orgId
); );
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek);
const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId }); 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" }); if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor.type, actor.type,
actor.id, actor.id,
key.projectId, key.projectId,
actor.authMethod, actor.authMethod,
actor.orgId actor.orgId
); );
ForbidOnInvalidProjectType(ProjectType.KMS);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek); 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.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
.select(selectAllTableCols(TableName.IdentityAccessToken)) .select(selectAllTableCols(TableName.IdentityAccessToken))
.select( .select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), 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.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
db.ref("name").withSchema(TableName.Identity) db.ref("name").withSchema(TableName.Identity)
) )
.first(); .first();
@ -61,7 +62,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });

View File

@ -171,7 +171,8 @@ export const identityAccessTokenServiceFactory = ({
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, [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]; 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("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("projectId").withSchema(TableName.IdentityProjectMembership), db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
db.ref("name").as("projectName").withSchema(TableName.Project), 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("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
@ -126,7 +127,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
createdAt, createdAt,
updatedAt, updatedAt,
projectId, projectId,
projectName projectName,
projectType
}) => ({ }) => ({
id, id,
identityId, identityId,
@ -147,7 +149,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
}, },
project: { project: {
id: projectId, id: projectId,
name: projectName name: projectName,
type: projectType
} }
}), }),
key: "id", key: "id",

View File

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

View File

@ -6,6 +6,7 @@ import {
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAzureAuths, TIdentityAzureAuths,
TIdentityGcpAuths, TIdentityGcpAuths,
TIdentityJwtAuths,
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOrgMemberships, TIdentityOrgMemberships,
@ -70,6 +71,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId` `${TableName.IdentityTokenAuth}.identityId`
) )
.leftJoin<TIdentityJwtAuths>(
TableName.IdentityJwtAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityJwtAuth}.identityId`
)
.select( .select(
selectAllTableCols(TableName.IdentityOrgMembership), selectAllTableCols(TableName.IdentityOrgMembership),
@ -81,6 +87,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), 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),
db.ref("name").withSchema(TableName.Identity) db.ref("name").withSchema(TableName.Identity)
); );
@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
`${TableName.IdentityTokenAuth}.identityId` `${TableName.IdentityTokenAuth}.identityId`
) )
.leftJoin<TIdentityJwtAuths>(
TableName.IdentityJwtAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityJwtAuth}.identityId`
)
.select( .select(
db.ref("id").withSchema("paginatedIdentity"), 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("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), 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 // cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@ -237,6 +250,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
uaId, uaId,
awsId, awsId,
gcpId, gcpId,
jwtId,
kubernetesId, kubernetesId,
oidcId, oidcId,
azureId, azureId,
@ -271,7 +285,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
kubernetesId, kubernetesId,
oidcId, oidcId,
azureId, 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 { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client";
import AWS from "aws-sdk"; 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TGenericPermission, TProjectPermission } from "@app/lib/types"; import { TGenericPermission, TProjectPermission } from "@app/lib/types";
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
@ -18,6 +26,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types"; import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list"; import { getApps } from "./integration-app-list";
import { TCircleCIContext } from "./integration-app-types";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import { import {
@ -25,6 +34,7 @@ import {
TBitbucketEnvironment, TBitbucketEnvironment,
TBitbucketWorkspace, TBitbucketWorkspace,
TChecklyGroups, TChecklyGroups,
TCircleCIOrganization,
TDeleteIntegrationAuthByIdDTO, TDeleteIntegrationAuthByIdDTO,
TDeleteIntegrationAuthsDTO, TDeleteIntegrationAuthsDTO,
TDuplicateGithubIntegrationAuthDTO, TDuplicateGithubIntegrationAuthDTO,
@ -36,6 +46,7 @@ import {
TIntegrationAuthBitbucketEnvironmentsDTO, TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthCircleCIOrganizationDTO,
TIntegrationAuthGithubEnvsDTO, TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO, TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO, TIntegrationAuthHerokuPipelinesDTO,
@ -145,13 +156,14 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations)) if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" }); throw new BadRequestError({ message: "Invalid integration" });
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const tokenExchange = await exchangeCode({ integration, code, url, installationId }); const tokenExchange = await exchangeCode({ integration, code, url, installationId });
@ -254,13 +266,14 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations)) if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" }); throw new BadRequestError({ message: "Invalid integration" });
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const updateDoc: TIntegrationAuthsInsert = { const updateDoc: TIntegrationAuthsInsert = {
@ -1570,6 +1583,120 @@ export const integrationAuthServiceFactory = ({
return []; return [];
}; };
const getCircleCIOrganizations = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TIntegrationAuthCircleCIOrganizationDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const { data: organizations }: { data: TCircleCIOrganization[] } = await request.get(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
{
headers: {
"Circle-Token": `${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
let projects: {
orgName: string;
projectName: string;
projectId?: string;
}[] = [];
try {
const projectRes = (
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
)
).data;
projects = projectRes.map((a) => ({
orgName: a.username, // username maps to unique organization name in CircleCI
projectName: a.reponame, // reponame maps to project name within an organization in CircleCI
projectId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
}));
} catch (error) {
logger.error(error);
}
const projectsByOrg = groupBy(
projects.map((p) => ({
orgName: p.orgName,
name: p.projectName,
id: p.projectId as string
})),
(p) => p.orgName
);
const getOrgContexts = async (orgSlug: string) => {
type NextPageToken = string | null | undefined;
try {
const contexts: TCircleCIContext[] = [];
let nextPageToken: NextPageToken;
while (nextPageToken !== null) {
// eslint-disable-next-line no-await-in-loop
const { data } = await request.get<{
items: TCircleCIContext[];
next_page_token: NextPageToken;
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
},
params: new URLSearchParams({
"owner-slug": orgSlug,
...(nextPageToken ? { "page-token": nextPageToken } : {})
})
});
contexts.push(...data.items);
nextPageToken = data.next_page_token;
}
return contexts?.map((context) => ({
name: context.name,
id: context.id
}));
} catch (error) {
logger.error(error);
}
};
return Promise.all(
organizations.map(async (org) => ({
name: org.name,
slug: org.slug,
projects: projectsByOrg[org.name] ?? [],
contexts: (await getOrgContexts(org.slug)) ?? []
}))
);
};
const deleteIntegrationAuths = async ({ const deleteIntegrationAuths = async ({
projectId, projectId,
integration, integration,
@ -1782,6 +1909,7 @@ export const integrationAuthServiceFactory = ({
getTeamcityBuildConfigs, getTeamcityBuildConfigs,
getBitbucketWorkspaces, getBitbucketWorkspaces,
getBitbucketEnvironments, getBitbucketEnvironments,
getCircleCIOrganizations,
getIntegrationAccessToken, getIntegrationAccessToken,
duplicateIntegrationAuth, duplicateIntegrationAuth,
getOctopusDeploySpaces, getOctopusDeploySpaces,

View File

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

View File

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

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 { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema"; import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types"; import {
CircleCiScope,
OctopusDeployScope,
TIntegrationsWithEnvironment,
TOctopusDeployVariableSet
} from "./integration-auth-types";
import { import {
IntegrationInitialSyncBehavior, IntegrationInitialSyncBehavior,
IntegrationMappingBehavior, IntegrationMappingBehavior,
Integrations, Integrations,
IntegrationUrls IntegrationUrls
} from "./integration-list"; } from "./integration-list";
import { isAzureKeyVaultReference } from "./integration-sync-secret-fns";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) => const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => { Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
@ -320,11 +326,12 @@ const syncSecretsAzureAppConfig = async ({
}; };
const metadata = IntegrationMetadataSchema.parse(integration.metadata); const metadata = IntegrationMetadataSchema.parse(integration.metadata);
const azureAppConfigSecrets = (
await getCompleteAzureAppConfigValues( const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
`${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*` metadata.azureLabel ? `&label=${metadata.azureLabel}` : ""
) }`;
).reduce(
const azureAppConfigSecrets = (await getCompleteAzureAppConfigValues(azureAppConfigValuesUrl)).reduce(
(accum, entry) => { (accum, entry) => {
accum[entry.key] = entry.value; accum[entry.key] = entry.value;
@ -405,14 +412,24 @@ const syncSecretsAzureAppConfig = async ({
} }
// create or update secrets on Azure App Config // create or update secrets on Azure App Config
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) { if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
await request.put( await request.put(
`${integration.app}/kv/${key}?api-version=2023-11-01`, `${integration.app}/kv/${key}?api-version=2023-11-01`,
{ {
value: secrets[key]?.value value: secrets[key]?.value,
...(isAzureKeyVaultReference(secrets[key]?.value || "") && {
content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
})
}, },
{ {
...(metadata.azureLabel && {
params: {
label: metadata.azureLabel
}
}),
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
}, },
@ -432,6 +449,11 @@ const syncSecretsAzureAppConfig = async ({
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
}, },
...(metadata.azureLabel && {
params: {
label: metadata.azureLabel
}
}),
// we force IPV4 because docker setup fails with ipv6 // we force IPV4 because docker setup fails with ipv6
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
family: 4 family: 4
@ -2245,102 +2267,174 @@ const syncSecretsCircleCI = async ({
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessToken: string; accessToken: string;
}) => { }) => {
const getProjectSlug = async () => { if (integration.scope === CircleCiScope.Context) {
const requestConfig = { // sync secrets to CircleCI
headers: { await Promise.all(
"Circle-Token": accessToken, Object.keys(secrets).map(async (key) =>
"Accept-Encoding": "application/json" request.put(
} `${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`,
}; {
value: secrets[key].value
try { },
const projectDetails = ( {
await request.get<{ slug: string }>( headers: {
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`, "Circle-Token": accessToken,
requestConfig "Content-Type": "application/json"
}
}
) )
).data; )
);
return projectDetails.slug; // get secrets from CircleCI
} catch (err) { const getSecretsRes = async () => {
if (err instanceof AxiosError) { type EnvVars = {
if (err.response?.data?.message !== "Not Found") { variable: string;
throw new Error("Failed to get project slug from CircleCI during first attempt."); created_at: string;
} updated_at: string;
} context_id: string;
} };
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization let nextPageToken: string | null | undefined;
try { const envVars: EnvVars[] = [];
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
// Case 1: This is a new integration where the organization name is stored under `integration.owner` while (nextPageToken !== null) {
if (integration.owner) { const res = await request.get<{
const org = circleCiOrganization.find((o) => o.name === integration.owner); items: EnvVars[];
if (org) { next_page_token: string | null;
return `${org.slug}/${integration.app}`; }>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, {
}
}
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
return `${circleCiOrganization[0].slug}/${integration.app}`;
} catch (err) {
throw new Error("Failed to get project slug from CircleCI during second attempt.");
}
};
const projectSlug = await getProjectSlug();
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
name: key,
value: secrets[key].value
},
{
headers: { headers: {
"Circle-Token": accessToken, "Circle-Token": accessToken,
"Content-Type": "application/json" "Accept-Encoding": "application/json"
} },
} params: nextPageToken
) ? new URLSearchParams({
) "page-token": nextPageToken
); })
: undefined
});
// get secrets from CircleCI envVars.push(...res.data.items);
const getSecretsRes = ( nextPageToken = res.data.next_page_token;
await request.get<{ items: { name: string }[] }>( }
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{ return envVars;
};
// delete secrets from CircleCI
await Promise.all(
(await getSecretsRes()).map(async (sec) => {
if (!(sec.variable in secrets)) {
return request.delete(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
);
}
})
);
} else {
const getProjectSlug = async () => {
const requestConfig = {
headers: { headers: {
"Circle-Token": accessToken, "Circle-Token": accessToken,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
} };
)
).data?.items;
// delete secrets from CircleCI try {
await Promise.all( const projectDetails = (
getSecretsRes.map(async (sec) => { await request.get<{ slug: string }>(
if (!(sec.name in secrets)) { `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, { requestConfig
)
).data;
return projectDetails.slug;
} catch (err) {
if (err instanceof AxiosError) {
if (err.response?.data?.message !== "Not Found") {
throw new Error("Failed to get project slug from CircleCI during first attempt.");
}
}
}
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
try {
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
if (integration.owner) {
const org = circleCiOrganization.find((o) => o.name === integration.owner);
if (org) {
return `${org.slug}/${integration.app}`;
}
}
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
return `${circleCiOrganization[0].slug}/${integration.app}`;
} catch (err) {
throw new Error("Failed to get project slug from CircleCI during second attempt.");
}
};
const projectSlug = await getProjectSlug();
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
name: key,
value: secrets[key].value
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
)
)
);
// get secrets from CircleCI
const getSecretsRes = (
await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
headers: { headers: {
"Circle-Token": accessToken, "Circle-Token": accessToken,
"Content-Type": "application/json" "Accept-Encoding": "application/json"
} }
}); }
} )
}) ).data?.items;
);
// delete secrets from CircleCI
await Promise.all(
getSecretsRes.map(async (sec) => {
if (!(sec.name in secrets)) {
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
});
}
})
);
}
}; };
/** /**

View File

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

View File

@ -1,5 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
@ -80,13 +81,14 @@ export const integrationServiceFactory = ({
if (!integrationAuth) if (!integrationAuth)
throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` }); throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` });
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
integrationAuth.projectId, integrationAuth.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@ -158,13 +160,14 @@ export const integrationServiceFactory = ({
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
integration.projectId, integration.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const newEnvironment = environment || integration.environment.slug; const newEnvironment = environment || integration.environment.slug;
@ -293,13 +296,14 @@ export const integrationServiceFactory = ({
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission( const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
integration.projectId, integration.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbidOnInvalidProjectType(ProjectType.SecretManager);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId); 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, TProjectUserMembershipRolesInsert,
TUsers TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; 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 { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TQueueServiceFactory } from "@app/queue";
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns"; import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; import { TAuthLoginFactory } from "../auth/auth-login-service";
import { ActorAuthMethod, ActorType, AuthMethod, AuthModeJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
@ -48,6 +49,10 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TSecretDALFactory } from "../secret/secret-dal";
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@ -70,6 +75,9 @@ import {
type TOrgServiceFactoryDep = { type TOrgServiceFactoryDep = {
userAliasDAL: Pick<TUserAliasDALFactory, "delete">; userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
orgBotDAL: TOrgBotDALFactory; orgBotDAL: TOrgBotDALFactory;
orgRoleDAL: TOrgRoleDALFactory; orgRoleDAL: TOrgRoleDALFactory;
@ -98,6 +106,8 @@ type TOrgServiceFactoryDep = {
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
}; };
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>; export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@ -105,6 +115,9 @@ export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
export const orgServiceFactory = ({ export const orgServiceFactory = ({
userAliasDAL, userAliasDAL,
orgDAL, orgDAL,
secretDAL,
secretV2BridgeDAL,
folderDAL,
userDAL, userDAL,
groupDAL, groupDAL,
orgRoleDAL, orgRoleDAL,
@ -125,7 +138,9 @@ export const orgServiceFactory = ({
projectBotDAL, projectBotDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityMetadataDAL, identityMetadataDAL,
projectBotService projectBotService,
queueService,
loginService
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@ -196,26 +211,18 @@ export const orgServiceFactory = ({
return org; return org;
}; };
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => { const findAllWorkspaces = async ({ actor, actorId, orgId, type }: 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;
}[];
})[];
if (actor === ActorType.USER) { if (actor === ActorType.USER) {
workspaces = await projectDAL.findAllProjects(actorId); const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all");
} else if (actor === ActorType.IDENTITY) { return workspaces;
workspaces = await projectDAL.findAllProjectsByIdentity(actorId);
} else {
throw new BadRequestError({ message: "Invalid actor type" });
} }
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) => { const addGhostUser = async (orgId: string, tx?: Knex) => {
@ -428,24 +435,88 @@ export const orgServiceFactory = ({
/* /*
* Delete organization by id * Delete organization by id
* */ * */
const deleteOrganizationById = async ( const deleteOrganizationById = async ({
userId: string, userId,
orgId: string, authorizationHeader,
actorAuthMethod: ActorAuthMethod, userAgentHeader,
actorOrgId: string | undefined ipAddress,
) => { orgId,
actorAuthMethod,
actorOrgId
}: {
userId: string;
authorizationHeader?: string;
userAgentHeader?: string;
ipAddress: string;
orgId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
}) => {
const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
name: "DeleteOrganizationById", name: "DeleteOrganizationById",
message: "Insufficient privileges" message: "Insufficient privileges"
}); });
const organization = await orgDAL.deleteById(orgId);
if (organization.customerId) {
await licenseService.removeOrgCustomer(organization.customerId);
} }
return organization;
if (!authorizationHeader) {
throw new UnauthorizedError({ name: "Authorization header not set on request." });
}
if (!userAgentHeader) {
throw new BadRequestError({ name: "User agent not set on request." });
}
const cfg = getConfig();
const authToken = authorizationHeader.replace("Bearer ", "");
const decodedToken = jwt.verify(authToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" });
const response = await orgDAL.transaction(async (tx) => {
const projects = await projectDAL.find({ orgId }, { tx });
for await (const project of projects) {
await fnDeleteProjectSecretReminders(project.id, {
secretDAL,
secretV2BridgeDAL,
queueService,
projectBotService,
folderDAL
});
}
const deletedOrg = await orgDAL.deleteById(orgId, tx);
if (deletedOrg.customerId) {
await licenseService.removeOrgCustomer(deletedOrg.customerId);
}
// Generate new tokens without the organization ID present
const user = await userDAL.findById(userId, tx);
const { access: accessToken, refresh: refreshToken } = await loginService.generateUserTokens(
{
user,
authMethod: decodedToken.authMethod,
ip: ipAddress,
userAgent: userAgentHeader,
isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
},
tx
);
return {
organization: deletedOrg,
tokens: {
accessToken,
refreshToken
}
};
});
return response;
}; };
/* /*
* Org membership management * Org membership management

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,14 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; 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 { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
export const projectDALFactory = (db: TDbClient) => { export const projectDALFactory = (db: TDbClient) => {
const projectOrm = ormify(db, TableName.Project); const projectOrm = ormify(db, TableName.Project);
const findAllProjects = async (userId: string) => { const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
try { try {
const workspaces = await db const workspaces = await db
.replicaNode()(TableName.ProjectMembership) .replicaNode()(TableName.ProjectMembership)
.where({ userId }) .where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .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`) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select( .select(
selectAllTableCols(TableName.Project), selectAllTableCols(TableName.Project),
@ -31,14 +44,17 @@ export const projectDALFactory = (db: TDbClient) => {
{ column: `${TableName.Environment}.position`, order: "asc" } { column: `${TableName.Environment}.position`, order: "asc" }
]); ]);
const groups: string[] = await db(TableName.UserGroupMembership) const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId");
.where({ userId })
.select(selectAllTableCols(TableName.UserGroupMembership))
.pluck("groupId");
const groupWorkspaces = await db(TableName.GroupProjectMembership) const groupWorkspaces = await db(TableName.GroupProjectMembership)
.whereIn("groupId", groups) .whereIn("groupId", groups)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) .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( .whereNotIn(
`${TableName.Project}.id`, `${TableName.Project}.id`,
workspaces.map(({ id }) => 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 { try {
const workspaces = await db const workspaces = await db
.replicaNode()(TableName.IdentityProjectMembership) .replicaNode()(TableName.IdentityProjectMembership)
.where({ identityId }) .where({ identityId })
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) .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`) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select( .select(
selectAllTableCols(TableName.Project), 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 { return {
...projectOrm, ...projectOrm,
findAllProjects, findAllProjects,
@ -325,6 +362,7 @@ export const projectDALFactory = (db: TDbClient) => {
findProjectByFilter, findProjectByFilter,
findProjectBySlug, findProjectBySlug,
findProjectWithOrg, findProjectWithOrg,
checkProjectUpgradeStatus checkProjectUpgradeStatus,
getProjectFromSplitId
}; };
}; };

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