1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-22 16:44:47 +00:00

Compare commits

..

140 Commits

Author SHA1 Message Date
58838c541f misc: added timeout for est connection 2024-08-27 23:26:56 +08:00
03cc71cfed Merge pull request from Infisical/feature/est-simpleenroll
Certificate EST protocol (simpleenroll, simplereenroll, cacerts)
2024-08-27 13:42:58 +08:00
02529106c9 Merge pull request from akhilmhdh/fix/scim-error
fix: resolved scim group update failing
2024-08-26 16:34:27 -04:00
=
d939ff289d fix: resolved scim group update failing 2024-08-27 01:50:26 +05:30
d1816c3051 Merge pull request from Infisical/daniel/azure-devops-docs
Docs: Azure DevOps Integration
2024-08-26 23:49:23 +04:00
cb350788c0 Update create.tsx 2024-08-26 23:21:56 +04:00
cd58768d6f Updated images 2024-08-26 23:20:51 +04:00
dcd6f4d55d Fix: Updated Azure DevOps integration styling 2024-08-26 23:12:00 +04:00
3c828614b8 Fix: Azure DevOps Label naming typos 2024-08-26 22:44:11 +04:00
09e7988596 Docs: Azure DevOps Integration 2024-08-26 22:43:49 +04:00
f40df19334 misc: finalized est config schema 2024-08-27 02:15:01 +08:00
76c9d3488b Merge remote-tracking branch 'origin/main' into feature/est-simpleenroll 2024-08-27 02:13:59 +08:00
0809da33e0 misc: improved docs and added support for curl clients 2024-08-27 02:05:35 +08:00
b528eec4bb Merge pull request from Infisical/daniel/secret-change-emails
Feat: Email notification on secret change requests
2024-08-26 21:50:30 +04:00
5179103680 Update SecretApprovalRequest.tsx 2024-08-26 21:46:05 +04:00
25a9e5f58a Update SecretApprovalRequest.tsx 2024-08-26 21:42:47 +04:00
8ddfe7b6e9 Update secret-approval-request-fns.ts 2024-08-26 20:53:43 +04:00
c23f21d57a Update SecretApprovalRequest.tsx 2024-08-26 20:21:05 +04:00
1242a43d98 Feat: Open approval with ID in URL 2024-08-26 20:04:06 +04:00
1655ca27d1 Fix: Creation of secret approval policies 2024-08-26 20:02:58 +04:00
2bcead03b0 Feat: Send secret change request emails to approvers 2024-08-26 19:55:04 +04:00
41ab1972ce Feat: Find project and include org dal 2024-08-26 19:54:48 +04:00
b00fff6922 Update index.ts 2024-08-26 19:54:15 +04:00
97b01ca5f8 Feat: Send secret change request emails to approvers 2024-08-26 19:54:01 +04:00
c2bd6f5ef3 Feat: Send secret change request emails to approvers 2024-08-26 19:53:49 +04:00
18efc9a6de Include more user details 2024-08-26 19:53:17 +04:00
436ccb25fb Merge pull request from Infisical/daniel/presist-selfhosting-domains
Feat: Persistent self-hosting domains on `infisical login`
2024-08-26 18:04:25 +04:00
8f08a352dd Merge pull request from Infisical/daniel/azure-devops-integration
Feat: Azure DevOps Integration
2024-08-26 18:04:04 +04:00
00f86cfd00 misc: addressed review comments 2024-08-26 21:10:29 +08:00
3944aafb11 Use slices 2024-08-26 15:18:45 +04:00
a6b852fab9 Fix: Type errors / cleanup 2024-08-26 15:13:18 +04:00
2a043afe11 Cleanup 2024-08-26 15:13:18 +04:00
df8f2cf9ab Update integration-sync-secret.ts 2024-08-26 15:13:18 +04:00
a18015b1e5 Fix: Use unique parameter for passing devops org name
Used to be teamId, now it's azureDevopsOrgName.
2024-08-26 15:13:18 +04:00
8b80622d2f Cleanup 2024-08-26 15:13:18 +04:00
c0fd0a56f3 Update integration-list.ts 2024-08-26 15:13:18 +04:00
326764dd41 Feat: Azure DevOps Integration 2024-08-26 15:13:18 +04:00
1f24d02c5e Fix: Do not save duplicate domains 2024-08-26 15:08:51 +04:00
c130fbddd9 Merge pull request from Infisical/daniel/specify-roles-and-projects
Feat: Select roles & projects when inviting members to organization
2024-08-26 15:00:39 +04:00
f560534493 Replace custom pkcs7 fns with module 2024-08-25 20:21:53 -07:00
10a97f4522 update python docs to point to new repo 2024-08-25 18:02:51 -04:00
7a2f0214f3 Feat: Persist self-hosting domains on infisical login 2024-08-24 13:18:03 +04:00
a2b994ab23 Requested changes 2024-08-24 11:00:05 +04:00
c4715124dc Merge pull request from Infisical/fix/resolve-name-null-null
fix: this pr addresses null null name issue with invited users
2024-08-23 14:01:27 -04:00
67c1cb9bf1 fix: this pr addresses null null name issue with invited users 2024-08-23 15:40:06 +08:00
68b1984a76 Merge pull request from Infisical/crl-update
CRL Distribution Point URLs + Support for Multiple CRLs per CA
2024-08-22 23:56:55 -07:00
ba45e83880 Clean 2024-08-22 23:37:36 -07:00
28ecc37163 Update org-service.ts 2024-08-23 03:10:14 +04:00
a6a2e2bae0 Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
d8bbfacae0 UI improvements 2024-08-23 02:25:15 +04:00
58549c398f Update project-service.ts 2024-08-23 02:25:15 +04:00
842ed62bec Rename 2024-08-23 02:25:15 +04:00
06d8800ee0 Feat: Specify organization role and projects when inviting users to org 2024-08-23 02:25:15 +04:00
2ecfd1bb7e Update auth-signup-type.ts 2024-08-23 02:25:15 +04:00
783d4c7bd6 Update org-dal.ts 2024-08-23 02:25:15 +04:00
fbf3f26abd Refactored org invites to allow for multiple users and to handle project invites 2024-08-23 02:25:15 +04:00
1d09693041 Update org-types.ts 2024-08-23 02:25:15 +04:00
626e37e3d0 Moved project membership creation to project membership fns 2024-08-23 02:25:15 +04:00
07fd67b328 Add metadata to SMTP email 2024-08-23 02:25:15 +04:00
3f1f018adc Update telemetry-types.ts 2024-08-23 02:25:15 +04:00
fe04e6d20c Remove *.*.posthog.com 2024-08-23 02:25:15 +04:00
d7171a1617 Removed unused code 2024-08-23 02:25:15 +04:00
384a0daa31 Update types.ts 2024-08-23 02:25:15 +04:00
c5c949e034 Multi user org invites 2024-08-23 02:25:15 +04:00
c2c9edf156 Update types.ts 2024-08-23 02:25:15 +04:00
c8248ef4e9 Fix: Skip org selection when user only has one org 2024-08-23 02:25:15 +04:00
9f6a6a7b7c Automatic timed toggle 2024-08-23 02:25:15 +04:00
121b642d50 Added new metadata parameter for signup 2024-08-23 02:25:15 +04:00
59b16f647e Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
2ab5932693 Update OrgMembersSection.tsx 2024-08-23 02:25:15 +04:00
8dfcef3900 Seperate component for Org Invite Links 2024-08-23 02:25:15 +04:00
8ca70eec44 Refactor add users to org handlers 2024-08-23 02:25:14 +04:00
60df59c7f0 Multi-user organization invites structure 2024-08-23 02:25:14 +04:00
e231c531a6 Update index.ts 2024-08-23 02:25:14 +04:00
d48bb910fa JWT invite lifetime (1 day) 2024-08-23 02:25:14 +04:00
1317266415 Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-22 14:54:34 -07:00
f0938330a7 Merge pull request from Infisical/daniel/disallow-user-creation-on-member-group-fix
Fix: Disallow org members to invite new members
2024-08-22 17:33:46 -04:00
e1bb0ac3ad Update org-permission.ts 2024-08-23 01:21:57 +04:00
f54d930de2 Fix: Disallow org members to invite new members 2024-08-23 01:13:45 +04:00
288f47f4bd Update API reference CRL docs 2024-08-22 12:30:48 -07:00
b090ebfd41 Update API reference CRL docs 2024-08-22 12:26:13 -07:00
67773bff5e Update wording on external parent ca 2024-08-22 12:18:00 -07:00
8ef1cfda04 Update docs for CRL 2024-08-22 12:16:37 -07:00
2a79d5ba36 Fix merge conflicts 2024-08-22 12:01:43 -07:00
0cb95f36ff Finish updating CRL impl 2024-08-22 11:55:19 -07:00
4a1dfda41f Merge pull request from Infisical/maidul-udfysfgj32
Remove service token depreciation notice
2024-08-22 14:29:55 -04:00
c238b7b6ae remove service token notice 2024-08-22 13:57:40 -04:00
288d7e88ae misc: made SSL header key configurable via env 2024-08-23 01:38:12 +08:00
83d314ba32 Merge pull request from Infisical/install-external-ca
Install Intermediate CA with External Parent CA
2024-08-22 09:41:52 -07:00
b94a0ffa6c Merge pull request from akhilmhdh/fix/build-mismatch-lines
feat: added backend build sourcemap for line matching
2024-08-22 09:34:12 -04:00
f88389bf9e misc: added general format 2024-08-22 21:05:34 +08:00
2e88c5e2c5 misc: improved url examples in est doc 2024-08-22 21:02:38 +08:00
73f3b8173e doc: added guide for EST usage' 2024-08-22 20:44:21 +08:00
=
b60e404243 feat: added backend build sourcemap for line matching 2024-08-22 15:18:33 +05:30
aa5b88ff04 misc: removed enrollment options from CA page 2024-08-22 15:40:36 +08:00
b7caff88cf feat: finished up EST cacerts 2024-08-22 15:39:53 +08:00
10120e1825 Merge pull request from akhilmhdh/feat/debounce-last-used
feat: added identity and service token postgres update debounced
2024-08-22 00:50:54 -04:00
31e66c18e7 Merge pull request from Infisical/maidul-deuyfgwyu
Set default to host sts endpoint for aws auth
2024-08-21 22:38:49 -04:00
fb06f5a3bc default to host sts for aws auth 2024-08-21 22:29:30 -04:00
1515dd8a71 Merge pull request from akhilmhdh/feat/ui-patch-v1
feat: resolved ui issues related to permission based hiding
2024-08-21 13:50:24 -04:00
=
da18a12648 fix: resovled create failing in bulk create too 2024-08-21 23:13:42 +05:30
=
49a0d3cec6 feat: resolved ui issues related to permission based hiding 2024-08-21 23:01:23 +05:30
=
e821a11271 feat: added identity and service token postgres update debounced 2024-08-21 22:21:31 +05:30
cf3b2ebbca Merge pull request from Infisical/daniel/read-secrets-notification-removal
Fix: Remove notification when unable to read secrets from environment
2024-08-20 21:56:41 +04:00
e970cc0f47 Fix: Error notification when user does not have access to read from certain environments 2024-08-20 21:41:02 +04:00
bd5cd03aeb Merge pull request from Infisical/daniel/access-requests-group-support
feat(core): Group support for access requests
2024-08-20 21:20:01 +04:00
760a1e917a feat: added simplereenroll 2024-08-20 23:56:27 +08:00
c46e4d7fc1 fix: scim cleanup 2024-08-20 19:50:42 +04:00
1f3896231a fix: remove privileges when user loses access to project/org 2024-08-20 19:50:42 +04:00
4323f6fa8f Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
65db91d491 Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
ae5b57f69f Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
b717de4f78 Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
1216d218c1 fix: rollback access approval requests requestedBy 2024-08-20 19:50:42 +04:00
209004ec6d fix: rollback access approval requests requestedBy 2024-08-20 19:50:42 +04:00
c865d12849 Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
c921c28185 Update AccessPolicyModal.tsx 2024-08-20 19:50:42 +04:00
3647943c80 Feat: Access requests group support 2024-08-20 19:50:42 +04:00
4bf5381060 Feat: Access requests group support 2024-08-20 19:50:42 +04:00
a10c358f83 Feat: Access requests group support 2024-08-20 19:50:42 +04:00
d3c63b5699 Access approval request 2024-08-20 19:50:42 +04:00
c64334462f Access approval policy 2024-08-20 19:50:42 +04:00
c497e19b99 Routers 2024-08-20 19:50:42 +04:00
2aeae616de Migration 2024-08-20 19:50:42 +04:00
e0e21530e2 Schemas 2024-08-20 19:50:41 +04:00
2d7ff66246 Merge branch 'feature/est-simpleenroll' of https://github.com/Infisical/infisical into feature/est-simpleenroll 2024-08-20 15:31:58 +08:00
179497e830 misc: moved est logic to service 2024-08-20 15:31:10 +08:00
4c08c80e5b Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-19 14:53:04 -07:00
7d6af64904 misc: added proxy header for amazon mtls client cert 2024-08-20 01:53:47 +08:00
16519f9486 feat: added reading SANs from CSR 2024-08-20 01:39:40 +08:00
bb27d38a12 misc: ui form adjustments 2024-08-19 21:39:00 +08:00
5b26928751 misc: added audit logs 2024-08-19 20:25:07 +08:00
f425e7e48f misc: addressed alignment issue 2024-08-19 19:50:09 +08:00
4601f46afb misc: finalized variable naming 2024-08-19 19:33:46 +08:00
692bdc060c misc: updated est configuration to be binded to certificate template 2024-08-19 19:26:20 +08:00
3a4f8c2e54 Merge branch 'feature/certificate-template' into feature/est-simpleenroll 2024-08-19 17:04:22 +08:00
146c4284a2 feat: integrated to est routes 2024-08-14 20:52:21 +08:00
5ae33b9f3b misc: minor UI updates 2024-08-14 01:10:25 +08:00
1f38b92ec6 feat: finished up integration for est config management 2024-08-14 01:00:31 +08:00
f2a49a79f0 feat: initial simpleenroll setup (mvp) 2024-08-13 23:22:47 +08:00
176 changed files with 6306 additions and 5375 deletions
.env.example
backend
package-lock.jsonpackage.json
src
@types
db
ee
keystore
lib
api-docs
config
dates
queue
server
services
cli/packages
docker-compose.dev.yml
docs
frontend
next.config.js
public/data
src
components
context/ProjectPermissionContext
hooks
pages
views
IntegrationsPage
Org
MembersPage/components/OrgMembersTab/components/OrgMembersSection
UserPage/components
Project
AuditLogsPage/components
CaPage
CertificatesPage/components
MembersPage/components/ServiceTokenTab
RolePage/components/RolePermissionsSection
SecretApprovalPage/components
AccessApprovalRequest
ApprovalPolicyList
SecretApprovalRequest
SecretOverviewPage
nginx

@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=

4580
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -34,9 +34,9 @@
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon",
"build": "tsup",
"build": "tsup --sourcemap",
"build:frontend": "npm run build --prefix ../frontend",
"start": "node dist/main.mjs",
"start": "node --enable-source-maps dist/main.mjs",
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
@ -126,7 +126,7 @@
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0",
"@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0",
"@team-plain/typescript-sdk": "^4.6.1",
@ -171,6 +171,7 @@
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"pkijs": "^3.2.4",
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"safe-regex": "^2.1.1",

@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
@ -160,6 +161,7 @@ declare module "fastify" {
certificateTemplate: TCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory;
pkiCollection: TPkiCollectionServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;

@ -53,6 +53,9 @@ import {
TCertificateSecretsUpdate,
TCertificatesInsert,
TCertificatesUpdate,
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate,
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate,
@ -372,6 +375,11 @@ declare module "knex/types/tables" {
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate
>;
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate
>;
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
TCertificateBodies,
TCertificateBodiesInsert,

@ -0,0 +1,294 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// ---------- ACCESS APPROVAL POLICY APPROVER ------------
const hasApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
const hasApproverId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverId");
if (!hasApproverUserId) {
// add the new fields
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
// if (hasApproverId) tb.setNullable("approverId");
tb.uuid("approverUserId");
tb.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
// convert project membership id => user id
await knex(TableName.AccessApprovalPolicyApprover).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.AccessApprovalPolicyApprover}.approverId`]))
});
// drop the old field
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
if (hasApproverId) tb.dropColumn("approverId");
tb.uuid("approverUserId").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST ------------
const hasAccessApprovalRequestTable = await knex.schema.hasTable(TableName.AccessApprovalRequest);
const hasRequestedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId");
const hasRequestedBy = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedBy");
if (hasAccessApprovalRequestTable) {
// new fields
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (!hasRequestedByUserId) {
tb.uuid("requestedByUserId");
tb.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
}
});
// copy the assigned project membership => user id to new fields
await knex(TableName.AccessApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
requestedByUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.AccessApprovalRequest}.requestedBy`]))
});
// drop old fields
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (hasRequestedBy) {
// DROP AT A LATER TIME
// tb.dropColumn("requestedBy");
// ADD ALLOW NULLABLE FOR NOW
tb.uuid("requestedBy").nullable().alter();
}
tb.uuid("requestedByUserId").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST REVIEWER ------------
const hasMemberId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "member");
const hasReviewerUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "reviewerUserId");
if (!hasReviewerUserId) {
// new fields
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
// if (hasMemberId) tb.setNullable("member");
tb.uuid("reviewerUserId");
tb.foreign("reviewerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
});
// copy project membership => user id to new fields
await knex(TableName.AccessApprovalRequestReviewer).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
reviewerUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.AccessApprovalRequestReviewer}.member`]))
});
// drop table
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
if (hasMemberId) {
// DROP AT A LATER TIME
// tb.dropColumn("member");
// ADD ALLOW NULLABLE FOR NOW
tb.uuid("member").nullable().alter();
}
tb.uuid("reviewerUserId").notNullable().alter();
});
}
// ---------- PROJECT USER ADDITIONAL PRIVILEGE ------------
const projectUserAdditionalPrivilegeHasProjectMembershipId = await knex.schema.hasColumn(
TableName.ProjectUserAdditionalPrivilege,
"projectMembershipId"
);
const projectUserAdditionalPrivilegeHasUserId = await knex.schema.hasColumn(
TableName.ProjectUserAdditionalPrivilege,
"userId"
);
if (!projectUserAdditionalPrivilegeHasUserId) {
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
tb.uuid("userId");
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
tb.string("projectId");
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
await knex(TableName.ProjectUserAdditionalPrivilege)
.update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
userId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`])),
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
projectId: knex(TableName.ProjectMembership)
.select("projectId")
.where("id", knex.raw("??", [`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`]))
})
.whereNotNull("projectMembershipId");
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
tb.uuid("userId").notNullable().alter();
tb.string("projectId").notNullable().alter();
});
}
if (projectUserAdditionalPrivilegeHasProjectMembershipId) {
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
// DROP AT A LATER TIME
// tb.dropColumn("projectMembershipId");
// ADD ALLOW NULLABLE FOR NOW
tb.uuid("projectMembershipId").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
// We remove project user additional privileges first, because it may delete records in the database where the project membership is not found.
// The project membership won't be found on records created by group members. In those cades we just delete the record and continue.
// When the additionl privilege record is deleted, it will cascade delete the access request created by the group member.
// ---------- PROJECT USER ADDITIONAL PRIVILEGE ------------
const hasUserId = await knex.schema.hasColumn(TableName.ProjectUserAdditionalPrivilege, "userId");
const hasProjectMembershipId = await knex.schema.hasColumn(
TableName.ProjectUserAdditionalPrivilege,
"projectMembershipId"
);
// If it doesn't have the userId field, then the up migration has not run
if (!hasUserId) {
return;
}
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
if (!hasProjectMembershipId) {
tb.uuid("projectMembershipId");
tb.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
});
if (!hasProjectMembershipId) {
// First, update records where a matching project membership exists
await knex(TableName.ProjectUserAdditionalPrivilege).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
projectMembershipId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.ProjectUserAdditionalPrivilege}.userId`]))
});
await knex(TableName.AccessApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
projectMembershipId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.userId`]))
});
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
tb.dropColumn("userId");
tb.dropColumn("projectId");
tb.uuid("projectMembershipId").notNullable().alter();
});
}
// Then, delete records where no matching project membership was found
await knex(TableName.ProjectUserAdditionalPrivilege).whereNull("projectMembershipId").delete();
await knex(TableName.AccessApprovalRequest).whereNull("requestedBy").delete();
// ---------- ACCESS APPROVAL POLICY APPROVER ------------
const hasApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
const hasApproverId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverId");
if (hasApproverUserId) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
if (!hasApproverId) {
tb.uuid("approverId");
tb.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
});
if (!hasApproverId) {
await knex(TableName.AccessApprovalPolicyApprover).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.AccessApprovalPolicyApprover}.approverUserId`]))
});
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
tb.dropColumn("approverUserId");
tb.uuid("approverId").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST ------------
const hasAccessApprovalRequestTable = await knex.schema.hasTable(TableName.AccessApprovalRequest);
const hasRequestedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId");
const hasRequestedBy = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedBy");
if (hasAccessApprovalRequestTable) {
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (!hasRequestedBy) {
tb.uuid("requestedBy");
tb.foreign("requestedBy").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
});
// Try to find a project membership based on the AccessApprovalRequest.requestedByUserId and AccessApprovalRequest.policyId(reference to AccessApprovalRequestPolicy).envId(reference to Environment).projectId(reference to Project)
// If a project membership is found, set the AccessApprovalRequest.requestedBy to the project membership id
// If a project membership is not found, remove the AccessApprovalRequest record
await knex(TableName.AccessApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
requestedBy: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.AccessApprovalRequest}.requestedByUserId`]))
});
// Then, delete records where no matching project membership was found
await knex(TableName.AccessApprovalRequest).whereNull("requestedBy").delete();
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (hasRequestedByUserId) {
tb.dropColumn("requestedByUserId");
}
if (hasRequestedBy) tb.uuid("requestedBy").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST REVIEWER ------------
const hasMemberId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "member");
const hasReviewerUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "reviewerUserId");
if (hasReviewerUserId) {
if (!hasMemberId) {
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
tb.uuid("member");
tb.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
});
}
await knex(TableName.AccessApprovalRequestReviewer).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
member: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`]))
});
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
tb.dropColumn("reviewerUserId");
tb.uuid("member").notNullable().alter();
});
}
}
}

@ -0,0 +1,26 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateTemplateEstConfig);
if (!hasEstConfigTable) {
await knex.schema.createTable(TableName.CertificateTemplateEstConfig, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.uuid("certificateTemplateId").notNullable().unique();
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
tb.binary("encryptedCaChain").notNullable();
tb.string("hashedPassphrase").notNullable();
tb.boolean("isEnabled").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.CertificateTemplateEstConfig);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}

@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCrl, "caSecretId");
if (!hasCaSecretIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").nullable();
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthorityCrl}" crl
SET "caSecretId" = (
SELECT sec.id
FROM "${TableName.CertificateAuthoritySecret}" sec
WHERE sec."caId" = crl."caId"
)
`);
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").notNullable().alter();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.dropColumn("caSecretId");
});
}
}

@ -9,10 +9,10 @@ import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesApproversSchema = z.object({
id: z.string().uuid(),
approverId: z.string().uuid(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
approverUserId: z.string().uuid()
});
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

@ -9,11 +9,12 @@ import { TImmutableDBKeys } from "./models";
export const AccessApprovalRequestsReviewersSchema = z.object({
id: z.string().uuid(),
member: z.string().uuid(),
member: z.string().uuid().nullable().optional(),
status: z.string(),
requestId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
reviewerUserId: z.string().uuid()
});
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;

@ -11,12 +11,13 @@ export const AccessApprovalRequestsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
privilegeId: z.string().uuid().nullable().optional(),
requestedBy: z.string().uuid(),
requestedBy: z.string().uuid().nullable().optional(),
isTemporary: z.boolean(),
temporaryRange: z.string().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
requestedByUserId: z.string().uuid()
});
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;

@ -14,7 +14,8 @@ export const CertificateAuthorityCrlSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
caId: z.string().uuid(),
encryptedCrl: zodBuffer
encryptedCrl: zodBuffer,
caSecretId: z.string().uuid()
});
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;

@ -0,0 +1,29 @@
// 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 CertificateTemplateEstConfigsSchema = z.object({
id: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
encryptedCaChain: zodBuffer,
hashedPassphrase: z.string(),
isEnabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
export type TCertificateTemplateEstConfigsInsert = Omit<
z.input<typeof CertificateTemplateEstConfigsSchema>,
TImmutableDBKeys
>;
export type TCertificateTemplateEstConfigsUpdate = Partial<
Omit<z.input<typeof CertificateTemplateEstConfigsSchema>, TImmutableDBKeys>
>;

@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret";
export * from "./certificate-bodies";
export * from "./certificate-secrets";
export * from "./certificate-template-est-configs";
export * from "./certificate-templates";
export * from "./certificates";
export * from "./dynamic-secret-leases";

@ -3,6 +3,7 @@ import { z } from "zod";
export enum TableName {
Users = "users",
CertificateAuthority = "certificate_authorities",
CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs",
CertificateAuthoritySecret = "certificate_authority_secret",
CertificateAuthorityCrl = "certificate_authority_crl",

@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const ProjectUserAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(),
slug: z.string(),
projectMembershipId: z.string().uuid(),
projectMembershipId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
@ -18,7 +18,9 @@ export const ProjectUserAdditionalPrivilegeSchema = z.object({
temporaryAccessEndTime: z.date().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
userId: z.string().uuid(),
projectId: z.string()
});
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;

@ -17,11 +17,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
name: z.string().optional(),
secretPath: z.string().trim().default("/"),
environment: z.string(),
approvers: z.string().array().min(1),
approverUserIds: z.string().array().min(1),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvals <= data.approverUserIds.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@ -56,7 +56,16 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
}),
response: {
200: z.object({
approvals: sapPubSchema.extend({ approvers: z.string().array(), secretPath: z.string().optional() }).array()
approvals: sapPubSchema
.extend({
userApprovers: z
.object({
userId: z.string()
})
.array(),
secretPath: z.string().optional().nullable()
})
.array()
})
}
},
@ -69,6 +78,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
projectSlug: req.query.projectSlug
});
return { approvals };
}
});
@ -117,11 +127,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.trim()
.optional()
.transform((val) => (val === "" ? "/" : val)),
approvers: z.string().array().min(1),
approverUserIds: z.string().array().min(1),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvals <= data.approverUserIds.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),

@ -1,10 +1,19 @@
import { z } from "zod";
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas";
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string() }).merge(
UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
username: true
})
);
export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
@ -104,10 +113,11 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
}),
reviewers: z
.object({
member: z.string(),
userId: z.string(),
status: z.string()
})
.array()
.array(),
requestedByUser: approvalRequestUser
}).array()
})
}

@ -1,86 +1,31 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { CA_CRLS } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:caId/crl",
url: "/:crlId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get CRL of the CA",
description: "Get CRL in DER format",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}),
response: {
200: z.object({
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
})
200: z.instanceof(Buffer)
}
},
handler: async (req) => {
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
handler: async (req, res) => {
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CRL,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
res.header("Content-Type", "application/pkix-crl");
return {
crl
};
return Buffer.from(crl);
}
});
// server.route({
// method: "GET",
// url: "/:caId/crl/rotate",
// config: {
// rateLimit: writeLimit
// },
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
// schema: {
// description: "Rotate CRL of the CA",
// params: z.object({
// caId: z.string().trim()
// }),
// response: {
// 200: z.object({
// message: z.string()
// })
// }
// },
// handler: async (req) => {
// await server.services.certificateAuthority.rotateCaCrl({
// caId: req.params.caId,
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId
// });
// return {
// message: "Successfully rotated CA CRL"
// };
// }
// });
};

@ -61,7 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
},
{ prefix: "/pki" }
);

@ -9,7 +9,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
@ -474,18 +477,18 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
Operations: z.array(
z.union([
z.object({
op: z.literal("replace"),
op: z.union([z.literal("replace"), z.literal("Replace")]),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({
op: z.literal("remove"),
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim()
}),
z.object({
op: z.literal("add"),
op: z.union([z.literal("add"), z.literal("Add")]),
path: z.string().trim(),
value: z.array(
z.object({

@ -1,9 +1,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TAccessApprovalPolicies } from "@app/db/schemas";
import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
@ -15,12 +15,12 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
// eslint-disable-next-line
.where(buildFindFilter(filter))
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.join(
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
@ -35,18 +35,30 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
});
const formatedDoc = mergeOneToManyRelation(
doc,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
const formattedDoc = sqlNestRelationships({
data: doc,
key: "id",
parentMapper: (data) => ({
environment: {
id: data.envId,
name: data.envName,
slug: data.envSlug
},
projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data)
}),
({ approverId }) => approverId,
"approvers"
);
return formatedDoc?.[0];
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
})
}
]
});
return formattedDoc?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindById" });
}
@ -55,18 +67,32 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
try {
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const formatedDoc = mergeOneToManyRelation(
docs,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
const formattedDocs = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
environment: {
id: data.envId,
name: data.envName,
slug: data.envSlug
},
projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data)
// secretPath: data.secretPath || undefined,
}),
({ approverId }) => approverId,
"approvers"
);
return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined }));
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
})
}
]
});
return formattedDocs;
} catch (error) {
throw new DatabaseError({ error, name: "Find" });
}

@ -34,8 +34,7 @@ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyApproverDAL,
permissionService,
projectEnvDAL,
projectDAL,
projectMembershipDAL
projectDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({
name,
@ -45,7 +44,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
approvals,
approvers,
approverUserIds,
projectSlug,
environment,
enforcementLevel
@ -53,7 +52,7 @@ export const accessApprovalPolicyServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (approvals > approvers.length)
if (approvals > approverUserIds.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
@ -70,15 +69,6 @@ export const accessApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new BadRequestError({ message: "Environment not found" });
const secretApprovers = await projectMembershipDAL.find({
projectId: project.id,
$in: { id: approvers }
});
if (secretApprovers.length !== approvers.length) {
throw new BadRequestError({ message: "Approver not found in project" });
}
await verifyApprovers({
projectId: project.id,
orgId: actorOrgId,
@ -86,7 +76,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
permissionService,
userIds: secretApprovers.map((approver) => approver.userId)
userIds: approverUserIds
});
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
@ -101,8 +91,8 @@ export const accessApprovalPolicyServiceFactory = ({
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
approverUserIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
@ -138,7 +128,7 @@ export const accessApprovalPolicyServiceFactory = ({
const updateAccessApprovalPolicy = async ({
policyId,
approvers,
approverUserIds,
secretPath,
name,
actorId,
@ -171,16 +161,7 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
if (approvers) {
// Find the workspace project memberships of the users passed in the approvers array
const secretApprovers = await projectMembershipDAL.find(
{
projectId: accessApprovalPolicy.projectId,
$in: { id: approvers }
},
{ tx }
);
if (approverUserIds) {
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
@ -188,15 +169,13 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: secretApprovers.map((approver) => approver.userId)
userIds: approverUserIds
});
if (secretApprovers.length !== approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
approverUserIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx

@ -17,7 +17,7 @@ export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
environment: string;
approvers: string[];
approverUserIds: string[];
projectSlug: string;
name: string;
enforcementLevel: EnforcementLevel;
@ -26,7 +26,7 @@ export type TCreateAccessApprovalPolicy = {
export type TUpdateAccessApprovalPolicy = {
policyId: string;
approvals?: number;
approvers?: string[];
approverUserIds?: string[];
secretPath?: string;
name?: string;
enforcementLevel?: EnforcementLevel;

@ -1,7 +1,7 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas";
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests, TUsers } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
@ -40,6 +40,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("requestedByUser"),
`${TableName.AccessApprovalRequest}.requestedByUserId`,
`requestedByUser.id`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
@ -52,7 +58,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
)
.select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(
db.ref("projectId").withSchema(TableName.Environment),
@ -61,15 +67,20 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
)
.select(
db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"),
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
)
// TODO: ADD SUPPORT FOR GROUPS!!!!
.select(
db
.ref("projectMembershipId")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeMembershipId"),
db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
@ -102,9 +113,18 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
enforcementLevel: doc.policyEnforcementLevel,
envId: doc.policyEnvId
},
requestedByUser: {
userId: doc.requestedByUserId,
email: doc.requestedByUserEmail,
firstName: doc.requestedByUserFirstName,
lastName: doc.requestedByUserLastName,
username: doc.requestedByUserUsername
},
privilege: doc.privilegeId
? {
membershipId: doc.privilegeMembershipId,
userId: doc.privilegeUserId,
projectId: doc.projectId,
isTemporary: doc.privilegeIsTemporary,
temporaryMode: doc.privilegeTemporaryMode,
temporaryRange: doc.privilegeTemporaryRange,
@ -118,11 +138,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
]
});
@ -146,30 +166,65 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicy}.id`
)
.join<TUsers>(
db(TableName.Users).as("requestedByUser"),
`${TableName.AccessApprovalRequest}.requestedByUserId`,
`requestedByUser.id`
)
.join(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id"
)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalReviewerUser"),
`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`,
`accessApprovalReviewerUser.id`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(
tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
tx.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"),
tx.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer),
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
tx.ref("email").withSchema("accessApprovalReviewerUser").as("reviewerEmail"),
tx.ref("username").withSchema("accessApprovalReviewerUser").as("reviewerUsername"),
tx.ref("firstName").withSchema("accessApprovalReviewerUser").as("reviewerFirstName"),
tx.ref("lastName").withSchema("accessApprovalReviewerUser").as("reviewerLastName"),
tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals")
);
const findById = async (id: string, tx?: Knex) => {
@ -189,15 +244,45 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel
},
requestedByUser: {
userId: el.requestedByUserId,
email: el.requestedByUserEmail,
firstName: el.requestedByUserFirstName,
lastName: el.requestedByUserLastName,
username: el.requestedByUserUsername
}
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
mapper: ({
reviewerUserId: userId,
reviewerStatus: status,
reviewerEmail: email,
reviewerLastName: lastName,
reviewerUsername: username,
reviewerFirstName: firstName
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({
approverUserId,
approverEmail: email,
approverUsername: username,
approverLastName: lastName,
approverFirstName: firstName
}) => ({
userId: approverUserId,
email,
firstName,
lastName,
username
})
}
]
});
if (!formatedDoc?.[0]) return;
@ -235,7 +320,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.where(`${TableName.Environment}.projectId`, projectId)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"));
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));
const formattedRequests = sqlNestRelationships({
data: accessRequests,
@ -245,9 +330,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
mapper: ({ reviewerUserId: reviewer, reviewerStatus: status }) =>
reviewer ? { reviewer, status } : undefined
}
]
});

@ -52,7 +52,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
>;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "findUserByProjectMembershipId" | "findUsersByProjectMembershipIds">;
userDAL: Pick<
TUserDALFactory,
"findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "find" | "findById"
>;
};
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
@ -94,7 +97,7 @@ export const accessApprovalRequestServiceFactory = ({
);
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id);
const requestedByUser = await userDAL.findById(actorId);
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
await projectDAL.checkProjectUpgradeStatus(project.id);
@ -114,13 +117,15 @@ export const accessApprovalRequestServiceFactory = ({
policyId: policy.id
});
const approverUsers = await userDAL.findUsersByProjectMembershipIds(
approvers.map((approver) => approver.approverId)
);
const approverUsers = await userDAL.find({
$in: {
id: approvers.map((approver) => approver.approverUserId)
}
});
const duplicateRequests = await accessApprovalRequestDAL.find({
policyId: policy.id,
requestedBy: membership.id,
requestedByUserId: actorId,
permissions: JSON.stringify(requestedPermissions),
isTemporary
});
@ -153,7 +158,7 @@ export const accessApprovalRequestServiceFactory = ({
const approvalRequest = await accessApprovalRequestDAL.create(
{
policyId: policy.id,
requestedBy: membership.id,
requestedByUserId: actorId,
temporaryRange: temporaryRange || null,
permissions: JSON.stringify(requestedPermissions),
isTemporary
@ -212,7 +217,7 @@ export const accessApprovalRequestServiceFactory = ({
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
if (authorProjectMembershipId) {
requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId);
requests = requests.filter((request) => request.requestedByUserId === actorId);
}
if (envSlug) {
@ -246,8 +251,8 @@ export const accessApprovalRequestServiceFactory = ({
if (
!hasRole(ProjectMembershipRole.Admin) &&
accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user
!policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
!policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver
) {
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
}
@ -273,7 +278,7 @@ export const accessApprovalRequestServiceFactory = ({
const review = await accessApprovalRequestReviewerDAL.findOne(
{
requestId: accessApprovalRequest.id,
member: membership.id
reviewerUserId: actorId
},
tx
);
@ -282,7 +287,7 @@ export const accessApprovalRequestServiceFactory = ({
{
status,
requestId: accessApprovalRequest.id,
member: membership.id
reviewerUserId: actorId
},
tx
);
@ -303,7 +308,8 @@ export const accessApprovalRequestServiceFactory = ({
// Permanent access
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.requestedBy,
userId: accessApprovalRequest.requestedByUserId,
projectId: accessApprovalRequest.projectId,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions)
},
@ -317,7 +323,8 @@ export const accessApprovalRequestServiceFactory = ({
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.requestedBy,
userId: accessApprovalRequest.requestedByUserId,
projectId: accessApprovalRequest.projectId,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions),
isTemporary: true,

@ -137,7 +137,7 @@ export enum EventType {
GET_CA_CERT = "get-certificate-authority-cert",
SIGN_INTERMEDIATE = "sign-intermediate",
IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl",
GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert",
GET_CERT = "get-cert",
@ -166,7 +166,10 @@ export enum EventType {
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
}
interface UserActorMetadata {
@ -1163,8 +1166,8 @@ interface ImportCaCert {
};
}
interface GetCaCrl {
type: EventType.GET_CA_CRL;
interface GetCaCrls {
type: EventType.GET_CA_CRLS;
metadata: {
caId: string;
dn: string;
@ -1420,6 +1423,29 @@ interface OrgAdminAccessProjectEvent {
}; // no metadata yet
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface UpdateCertificateTemplateEstConfig {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface GetCertificateTemplateEstConfig {
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -1518,7 +1544,7 @@ export type Event =
| GetCaCert
| SignIntermediate
| ImportCaCert
| GetCaCrl
| GetCaCrls
| IssueCert
| SignCert
| GetCert
@ -1547,4 +1573,7 @@ export type Event =
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate;
| DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig;

@ -2,24 +2,24 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TGetCrl } from "./certificate-authority-crl-types";
import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
type TCertificateAuthorityCrlServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
@ -29,13 +29,42 @@ export const certificateAuthorityCrlServiceFactory = ({
certificateAuthorityCrlDAL,
projectDAL,
kmsService,
permissionService,
licenseService
permissionService // licenseService
}: TCertificateAuthorityCrlServiceFactoryDep) => {
/**
* Return the Certificate Revocation List (CRL) for CA with id [caId]
* Return CRL with id [crlId]
*/
const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => {
const getCrlById = async (crlId: TGetCrlById) => {
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
if (!caCrl) throw new NotFoundError({ message: "CRL not found" });
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
return {
ca,
caCrl,
crl: crl.rawData
};
};
/**
* Returns a list of CRL ids for CA with id [caId]
*/
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
@ -52,15 +81,14 @@ export const certificateAuthorityCrlServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.caCrl)
throw new BadRequestError({
message:
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
});
// const plan = await licenseService.getPlan(actorOrgId);
// if (!plan.caCrl)
// throw new BadRequestError({
// message:
// "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
// });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id });
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@ -72,15 +100,23 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId
});
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const decryptedCrls = await Promise.all(
caCrls.map(async (caCrl) => {
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
return {
id: caCrl.id,
crl: crlPem
};
})
);
return {
crl: crlPem,
ca
ca,
crls: decryptedCrls
};
};
@ -166,7 +202,8 @@ export const certificateAuthorityCrlServiceFactory = ({
// };
return {
getCaCrl
getCrlById,
getCaCrls
// rotateCaCrl
};
};

@ -1,5 +1,7 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetCrl = {
export type TGetCrlById = string;
export type TGetCaCrlsDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;

@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type,
version: 1,

@ -126,7 +126,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);

@ -66,6 +66,7 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
@ -73,6 +74,12 @@ export const permissionDALFactory = (db: TDbClient) => {
)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.GroupProjectMembership}.projectId`,
`${TableName.Project}.id`
)
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
@ -81,9 +88,30 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
)
.select("permissions");
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles).as("permissions"),
// db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("apPermissions")
// Additional Privileges
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessEndTime")
);
// .select(`${TableName.ProjectRoles}.permissions`);
const docs = await db(TableName.ProjectMembership)
.join(
@ -98,12 +126,13 @@ export const permissionDALFactory = (db: TDbClient) => {
)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
`${TableName.ProjectUserAdditionalPrivilege}.projectId`,
`${TableName.ProjectMembership}.projectId`
)
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId)
.where(`${TableName.ProjectMembership}.userId`, userId)
.where(`${TableName.ProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
.select(
@ -120,6 +149,10 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
@ -198,6 +231,31 @@ export const permissionDALFactory = (db: TDbClient) => {
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
},
{
key: "userApId",
label: "additionalPrivileges" as const,
mapper: ({
userApId,
userApProjectId,
userApUserId,
userApPermissions,
userApIsTemporary,
userApTemporaryMode,
userApTemporaryRange,
userApTemporaryAccessEndTime,
userApTemporaryAccessStartTime
}) => ({
id: userApId,
userId: userApUserId,
projectId: userApProjectId,
permissions: userApPermissions,
temporaryRange: userApTemporaryRange,
temporaryMode: userApTemporaryMode,
temporaryAccessEndTime: userApTemporaryAccessEndTime,
temporaryAccessStartTime: userApTemporaryAccessStartTime,
isTemporary: userApIsTemporary
})
}
]
})
@ -218,15 +276,24 @@ export const permissionDALFactory = (db: TDbClient) => {
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
const activeAdditionalPrivileges =
permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeGroupAdditionalPrivileges =
groupPermission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime, userId: apUserId, projectId: apProjectId }) =>
apProjectId === projectId &&
apUserId === userId &&
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
) ?? [];
return {
...(permission[0] || groupPermission[0]),
roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: activeAdditionalPrivileges
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
};
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" });

@ -18,7 +18,7 @@ import {
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@ -53,12 +53,17 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId });
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug,
projectId: projectMembership.projectId,
userId: projectMembership.userId
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
if (!dto.isTemporary) {
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
projectMembershipId,
userId: projectMembership.userId,
projectId: projectMembership.projectId,
slug,
permissions: customPermission
});
@ -67,7 +72,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
projectMembershipId,
projectId: projectMembership.projectId,
userId: projectMembership.userId,
slug,
permissions: customPermission,
isTemporary: true,
@ -90,7 +96,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
@ -105,7 +115,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
if (dto?.slug) {
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug: dto.slug,
projectMembershipId: projectMembership.id
userId: projectMembership.id,
projectId: projectMembership.projectId
});
if (existingSlug && existingSlug.id !== userPrivilege.id)
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
@ -138,7 +149,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
@ -164,7 +178,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
@ -198,7 +215,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({ projectMembershipId });
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({
userId: projectMembership.userId,
projectId: projectMembership.projectId
});
return userPrivileges;
};

@ -50,8 +50,8 @@ export const buildScimUser = ({
orgMembershipId: string;
username: string;
email?: string | null;
firstName: string;
lastName: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
groups?: {
value: string;
display: string;
@ -64,9 +64,9 @@ export const buildScimUser = ({
userName: username,
displayName: `${firstName} ${lastName}`,
name: {
givenName: firstName,
givenName: firstName || "",
middleName: null,
familyName: lastName
familyName: lastName || ""
},
emails: email
? [

@ -31,6 +31,7 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import {
buildScimGroup,
buildScimGroupList,
@ -93,6 +94,7 @@ type TScimServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: Pick<TSmtpService, "sendMail">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
};
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
@ -112,6 +114,7 @@ export const scimServiceFactory = ({
projectKeyDAL,
projectBotDAL,
permissionService,
projectUserAdditionalPrivilegeDAL,
smtpService
}: TScimServiceFactoryDep) => {
const createScimToken = async ({
@ -264,8 +267,8 @@ export const scimServiceFactory = ({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email ?? "",
firstName: membership.firstName as string,
lastName: membership.lastName as string,
firstName: membership.firstName,
lastName: membership.lastName,
active: membership.isActive,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
@ -424,8 +427,8 @@ export const scimServiceFactory = ({
return buildScimUser({
orgMembershipId: createdOrgMembership.id,
username: externalId,
firstName: createdUser.firstName as string,
lastName: createdUser.lastName as string,
firstName: createdUser.firstName,
lastName: createdUser.lastName,
email: createdUser.email ?? "",
active: createdOrgMembership.isActive
});
@ -480,8 +483,8 @@ export const scimServiceFactory = ({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
firstName: membership.firstName,
lastName: membership.lastName,
active
});
};
@ -524,8 +527,8 @@ export const scimServiceFactory = ({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName as string,
lastName: membership.lastName as string,
firstName: membership.firstName,
lastName: membership.lastName,
active,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
@ -558,6 +561,7 @@ export const scimServiceFactory = ({
orgId: membership.orgId,
orgDAL,
projectMembershipDAL,
projectUserAdditionalPrivilegeDAL,
projectKeyDAL,
userAliasDAL,
licenseService
@ -880,59 +884,50 @@ export const scimServiceFactory = ({
}
for await (const operation of operations) {
switch (operation.op) {
case "replace": {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
if (operation.op === "replace" || operation.op === "Replace") {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
});
} else if (operation.op === "add" || operation.op === "Add") {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
break;
}
case "add": {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
await addUsersToGroupByUserIds({
group,
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
break;
}
case "remove": {
const orgMembershipId = extractScimValueFromPath(operation.path);
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
await removeUsersFromGroupByUserIds({
await addUsersToGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL
});
break;
}
default: {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
} else if (operation.op === "remove" || operation.op === "Remove") {
const orgMembershipId = extractScimValueFromPath(operation.path);
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
await removeUsersFromGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
} else {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
}
}

@ -110,8 +110,10 @@ export type TUpdateScimGroupNamePatchDTO = {
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
};
// akhilmhdh: I know, this is done due to lack of time. Need to change later to support as normalized rather than like this
// Forgive akhil blame tony
type TReplaceOp = {
op: "replace";
op: "replace" | "Replace";
value: {
id: string;
displayName: string;
@ -119,12 +121,12 @@ type TReplaceOp = {
};
type TRemoveOp = {
op: "remove";
op: "remove" | "Remove";
path: string;
};
type TAddOp = {
op: "add";
op: "add" | "Add";
path: string;
value: {
value: string;

@ -20,7 +20,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
)
.select(
tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
@ -47,8 +55,11 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
userId: approverUserId,
email: approverEmail,
firstName: approverFirstName,
lastName: approverLastName
})
}
]

@ -0,0 +1,46 @@
import { TSecretApprovalRequests } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
type TSendApprovalEmails = {
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectWithOrg">;
smtpService: Pick<TSmtpService, "sendMail">;
projectId: string;
secretApprovalRequest: TSecretApprovalRequests;
};
export const sendApprovalEmailsFn = async ({
secretApprovalPolicyDAL,
projectDAL,
smtpService,
projectId,
secretApprovalRequest
}: TSendApprovalEmails) => {
const cfg = getConfig();
const policy = await secretApprovalPolicyDAL.findById(secretApprovalRequest.policyId);
const project = await projectDAL.findProjectWithOrg(projectId);
// now we need to go through each of the reviewers and print out all the commits that they need to approve
for await (const reviewerUser of policy.userApprovers) {
await smtpService.sendMail({
recipients: [reviewerUser?.email as string],
subjectLine: "Infisical Secret Change Request",
substitutions: {
firstName: reviewerUser.firstName,
projectName: project.name,
organizationName: project.organization.name,
approvalUrl: `${cfg.isDevelopmentMode ? "https" : "http"}://${cfg.SITE_URL}/project/${
project.id
}/approval?requestId=${secretApprovalRequest.id}`
},
template: SmtpTemplates.SecretApprovalRequestNeedsReview
});
}
};

@ -53,8 +53,10 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
import {
@ -89,7 +91,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
projectDAL: Pick<
TProjectDALFactory,
"checkProjectUpgradeStatus" | "findById" | "findProjectById" | "findProjectWithOrg"
>;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick<
@ -98,6 +103,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@ -121,6 +127,7 @@ export const secretApprovalRequestServiceFactory = ({
smtpService,
userDAL,
projectEnvDAL,
secretApprovalPolicyDAL,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
@ -1061,6 +1068,15 @@ export const secretApprovalRequestServiceFactory = ({
}
return { ...doc, commits: approvalCommits };
});
await sendApprovalEmailsFn({
projectDAL,
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
});
return secretApprovalRequest;
};
@ -1311,8 +1327,17 @@ export const secretApprovalRequestServiceFactory = ({
tx
);
}
return { ...doc, commits: approvalCommits };
});
await sendApprovalEmailsFn({
projectDAL,
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
});
return secretApprovalRequest;
};

@ -19,11 +19,15 @@ export const KeyStorePrefixes = {
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`,
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
};
export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10,
AccessTokenStatusUpdateInSeconds: 120
};
type TWaitTillReady = {

@ -1120,9 +1120,10 @@ export const CERTIFICATE_AUTHORITIES = {
certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
GET_CRL: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
crl: "The certificate revocation list (CRL) of the CA"
GET_CRLS: {
caId: "The ID of the CA to get the certificate revocation lists (CRLs) for",
id: "The ID of certificate revocation list (CRL)",
crl: "The certificate revocation list (CRL)"
}
};
@ -1174,6 +1175,13 @@ export const CERTIFICATE_TEMPLATES = {
}
};
export const CA_CRLS = {
GET: {
crlId: "The ID of the certificate revocation list (CRL) to get",
crl: "The certificate revocation list (CRL)"
}
};
export const ALERTS = {
CREATE: {
projectId: "The ID of the project to create the alert in",

@ -74,6 +74,7 @@ const envSchema = z
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")),
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
// Oauth
@ -141,7 +142,8 @@ const envSchema = z
CAPTCHA_SECRET: zpStr(z.string().optional()),
PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert")
})
.transform((data) => ({
...data,

@ -1,3 +1,8 @@
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const secondsToMillis = (seconds: number) => seconds * 1000;
export const applyJitter = (delayMs: number, jitterMs: number) => {
const jitter = Math.floor(Math.random() * (2 * jitterMs)) - jitterMs;
return delayMs + jitter;
};

@ -27,7 +27,8 @@ export enum QueueName {
CaCrlRotation = "ca-crl-rotation",
SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration"
ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update"
}
export enum QueueJobs {
@ -48,7 +49,9 @@ export enum QueueJobs {
CaCrlRotation = "ca-crl-rotation-job",
SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration"
ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update"
}
export type TQueueJobTypes = {
@ -148,6 +151,15 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration;
payload: { projectId: string };
};
[QueueName.AccessTokenStatusUpdate]:
| {
name: QueueJobs.IdentityAccessTokenStatusUpdate;
payload: { identityAccessTokenId: string; numberOfUses: number };
}
| {
name: QueueJobs.ServiceTokenStatusUpdate;
payload: { serviceTokenId: string };
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

@ -57,7 +57,6 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
}
const authHeader = req.headers?.authorization;
if (!authHeader) return { authMode: null, token: null };
const authTokenValue = authHeader.slice(7); // slice of after Bearer
@ -103,12 +102,13 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
if (req.url.includes("/api/v3/auth/")) {
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) {
return;
}
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
if (!authMode) return;
switch (authMode) {

@ -0,0 +1,173 @@
import bcrypt from "bcrypt";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
// add support for CSR bodies
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
try {
let csrBody = body as string;
// some EST clients send CSRs in PEM format and some in base64 format
// for CSRs sent in PEM, we leave them as is
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
}
done(null, csrBody);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
// Authenticate EST client using Passphrase
server.addHook("onRequest", async (req, res) => {
const { authorization } = req.headers;
const urlFragments = req.url.split("/");
// cacerts endpoint should not have any authentication
if (urlFragments[urlFragments.length - 1] === "cacerts") {
return;
}
if (!authorization) {
const wwwAuthenticateHeader = "WWW-Authenticate";
const errAuthRequired = "Authentication required";
await res.hijack();
// definitive connection timeout to clean-up open connections and prevent memory leak
res.raw.setTimeout(10 * 1000, () => {
res.raw.end();
});
res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`);
res.raw.setHeader("Content-Length", 0);
res.raw.statusCode = 401;
// Write the error message to the response without ending the connection
res.raw.write(errAuthRequired);
// flush headers
res.raw.flushHeaders();
return;
}
const certificateTemplateId = urlFragments.slice(-2)[0];
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const rawCredential = authorization?.split(" ").pop();
if (!rawCredential) {
throw new UnauthorizedError({ message: "Missing HTTP credentials" });
}
// expected format is user:password
const basicCredential = atob(rawCredential);
const password = basicCredential.split(":").pop();
if (!password) {
throw new BadRequestError({
message: "No password provided"
});
}
const isPasswordValid = await bcrypt.compare(password, estConfig.hashedPassphrase);
if (!isPasswordValid) {
throw new UnauthorizedError({
message: "Invalid credentials"
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simpleenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleEnroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simplereenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleReenroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "GET",
url: "/:certificateTemplateId/cacerts",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.getCaCerts({
certificateTemplateId: req.params.certificateTemplateId
});
}
});
};

@ -73,6 +73,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { authDALFactory } from "@app/services/auth/auth-dal";
@ -89,7 +90,9 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
@ -194,6 +197,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerCertificateEstRouter } from "./est/certificate-est-router";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
@ -412,6 +416,7 @@ export const registerRoutes = async (
orgDAL,
orgMembershipDAL,
projectDAL,
projectUserAdditionalPrivilegeDAL,
projectMembershipDAL,
groupDAL,
groupProjectDAL,
@ -475,8 +480,12 @@ export const registerRoutes = async (
orgRoleDAL,
permissionService,
orgDAL,
userGroupMembershipDAL,
projectBotDAL,
incidentContactDAL,
tokenService,
projectUserAdditionalPrivilegeDAL,
projectUserMembershipRoleDAL,
projectDAL,
projectMembershipDAL,
orgMembershipDAL,
@ -496,6 +505,8 @@ export const registerRoutes = async (
projectDAL,
projectBotDAL,
groupProjectDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
orgDAL,
orgService,
licenseService
@ -549,10 +560,12 @@ export const registerRoutes = async (
projectBotDAL,
orgDAL,
userDAL,
projectUserAdditionalPrivilegeDAL,
userGroupMembershipDAL,
smtpService,
projectKeyDAL,
projectRoleDAL,
groupProjectDAL,
licenseService
});
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
@ -590,6 +603,7 @@ export const registerRoutes = async (
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
const certificateTemplateDAL = certificateTemplateDALFactory(db);
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db);
@ -641,14 +655,27 @@ export const registerRoutes = async (
certificateAuthorityCrlDAL,
projectDAL,
kmsService,
permissionService,
licenseService
permissionService
// licenseService
});
const certificateTemplateService = certificateTemplateServiceFactory({
certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL,
permissionService
permissionService,
kmsService,
projectDAL
});
const certificateEstService = certificateEstServiceFactory({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const pkiAlertService = pkiAlertServiceFactory({
@ -678,6 +705,7 @@ export const registerRoutes = async (
orgDAL,
orgService,
projectMembershipDAL,
projectRoleDAL,
folderDAL,
licenseService,
certificateAuthorityDAL,
@ -834,6 +862,7 @@ export const registerRoutes = async (
secretQueueService,
kmsService,
secretV2BridgeDAL,
secretApprovalPolicyDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
smtpService,
@ -949,12 +978,20 @@ export const registerRoutes = async (
kmsService
});
const accessTokenQueue = accessTokenQueueServiceFactory({
keyStore,
identityAccessTokenDAL,
queueService,
serviceTokenDAL
});
const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL,
serviceTokenDAL,
userDAL,
permissionService,
projectDAL
projectDAL,
accessTokenQueue
});
const identityService = identityServiceFactory({
@ -964,10 +1001,13 @@ export const registerRoutes = async (
identityProjectDAL,
licenseService
});
const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL,
identityOrgMembershipDAL
identityOrgMembershipDAL,
accessTokenQueue
});
const identityProjectService = identityProjectServiceFactory({
permissionService,
projectDAL,
@ -1173,6 +1213,7 @@ export const registerRoutes = async (
certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService,
pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService,
secretScanning: secretScanningService,
@ -1240,6 +1281,9 @@ export const registerRoutes = async (
}
});
// register special routes
await server.register(registerCertificateEstRouter, { prefix: "/.well-known/est" });
// register routes for v1
await server.register(
async (v1Server) => {

@ -669,6 +669,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
isInternal: false,
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@ -691,11 +692,90 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
});
return {
certificate,
certificate: certificate.toString("pem"),
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
server.route({
method: "GET",
url: "/:caId/crls",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get list of CRLs of the CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId)
}),
response: {
200: z.array(
z.object({
id: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.id),
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.crl)
})
)
}
},
handler: async (req) => {
const { ca, crls } = await server.services.certificateAuthorityCrl.getCaCrls({
caId: req.params.caId,
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_CA_CRLS,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return crls;
}
});
// TODO: implement this endpoint in the future
// server.route({
// method: "GET",
// url: "/:caId/crl/rotate",
// config: {
// rateLimit: writeLimit
// },
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
// schema: {
// description: "Rotate CRLs of the CA",
// params: z.object({
// caId: z.string().trim()
// }),
// response: {
// 200: z.object({
// message: z.string()
// })
// }
// },
// handler: async (req) => {
// await server.services.certificateAuthority.rotateCaCrl({
// caId: req.params.caId,
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId
// });
// return {
// message: "Successfully rotated CA CRL"
// };
// }
// });
};

@ -210,6 +210,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
isInternal: false,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@ -231,7 +232,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
});
return {
certificate,
certificate: certificate.toString("pem"),
certificateChain,
issuingCaCertificate,
serialNumber

@ -1,6 +1,7 @@
import ms from "ms";
import { z } from "zod";
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@ -9,6 +10,12 @@ import { AuthMode } from "@app/services/auth/auth-type";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
id: true,
certificateTemplateId: true,
isEnabled: true
});
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@ -202,4 +209,141 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
return certificateTemplate;
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1),
passphrase: z.string().min(1),
isEnabled: z.boolean().default(true)
}),
response: {
200: sanitizedEstConfig
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.createEstConfiguration({
certificateTemplateId: req.params.certificateTemplateId,
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: estConfig.projectId,
event: {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId,
isEnabled: estConfig.isEnabled as boolean
}
}
});
return estConfig;
}
});
server.route({
method: "PATCH",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1).optional(),
passphrase: z.string().min(1).optional(),
isEnabled: z.boolean().optional()
}),
response: {
200: sanitizedEstConfig
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.updateEstConfiguration({
certificateTemplateId: req.params.certificateTemplateId,
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: estConfig.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId,
isEnabled: estConfig.isEnabled as boolean
}
}
});
return estConfig;
}
});
server.route({
method: "GET",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
response: {
200: sanitizedEstConfig.extend({
caChain: z.string()
})
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: false,
certificateTemplateId: 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: estConfig.projectId,
event: {
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId
}
}
});
return estConfig;
}
});
};

@ -293,6 +293,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}),
querystring: z.object({
teamId: z.string().trim().optional(),
azureDevOpsOrgName: z.string().trim().optional(),
workspaceSlug: z.string().trim().optional()
}),
response: {

@ -1,6 +1,6 @@
import { z } from "zod";
import { UsersSchema } from "@app/db/schemas";
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -16,23 +16,37 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
method: "POST",
schema: {
body: z.object({
inviteeEmail: z.string().trim().email(),
organizationId: z.string().trim()
inviteeEmails: z.array(z.string().trim().email()),
organizationId: z.string().trim(),
projectIds: z.array(z.string().trim()).optional(),
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
}),
response: {
200: z.object({
message: z.string(),
completeInviteLink: z.string().optional()
completeInviteLinks: z
.array(
z.object({
email: z.string(),
link: z.string()
})
)
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return;
const completeInviteLink = await server.services.org.inviteUserToOrganization({
const completeInviteLinks = await server.services.org.inviteUserToOrganization({
orgId: req.body.organizationId,
userId: req.permission.id,
inviteeEmail: req.body.inviteeEmail,
inviteeEmails: req.body.inviteeEmails,
projectIds: req.body.projectIds,
projectRoleSlug: req.body.projectRoleSlug,
organizationRoleSlug: req.body.organizationRoleSlug,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
@ -41,14 +55,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
event: PostHogEventTypes.UserOrgInvitation,
distinctId: getTelemetryDistinctId(req),
properties: {
inviteeEmail: req.body.inviteeEmail,
inviteeEmails: req.body.inviteeEmails,
organizationRoleSlug: req.body.organizationRoleSlug,
...req.auditLogInfo
}
});
return {
completeInviteLink,
message: `Send an invite link to ${req.body.inviteeEmail}`
completeInviteLinks,
message: `Send an invite link to ${req.body.inviteeEmails.join(", ")}`
};
}
});

@ -1,6 +1,12 @@
import { z } from "zod";
import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import {
IntegrationsSchema,
ProjectMembershipsSchema,
ProjectRolesSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -59,12 +65,19 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
querystring: z.object({
includeGroupMembers: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}),
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
users: ProjectMembershipsSchema.extend({
isGroupMember: z.boolean(),
user: UsersSchema.pick({
email: true,
username: true,
@ -99,9 +112,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
includeGroupMembers: req.query.includeGroupMembers,
projectId: req.params.workspaceId,
actorOrgId: req.permission.orgId
});
return { users };
}
});
@ -113,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
querystring: z.object({
includeRoles: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}),
response: {
200: z.object({
workspaces: projectWithEnv.array()
workspaces: projectWithEnv
.extend({
roles: ProjectRolesSchema.array().optional()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const workspaces = await server.services.project.getProjects(req.permission.id);
const workspaces = await server.services.project.getProjects({
includeRoles: req.query.includeRoles,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
return { workspaces };
}
});

@ -179,7 +179,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
verifier: z.string().trim(),
tokenMetadata: z.string().optional()
}),
response: {
200: z.object({

@ -0,0 +1,125 @@
import { z } from "zod";
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { applyJitter, secondsToMillis } from "@app/lib/dates";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TServiceTokenDALFactory } from "../service-token/service-token-dal";
type TAccessTokenQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "updateById">;
serviceTokenDAL: Pick<TServiceTokenDALFactory, "updateById">;
};
export type TAccessTokenQueueServiceFactory = ReturnType<typeof accessTokenQueueServiceFactory>;
export const AccessTokenStatusSchema = z.object({
lastUpdatedAt: z.string().datetime(),
numberOfUses: z.number()
});
export const accessTokenQueueServiceFactory = ({
queueService,
keyStore,
identityAccessTokenDAL,
serviceTokenDAL
}: TAccessTokenQueueServiceFactoryDep) => {
const getIdentityTokenDetailsInCache = async (identityAccessTokenId: string) => {
const tokenDetailsInCache = await keyStore.getItem(
KeyStorePrefixes.IdentityAccessTokenStatusUpdate(identityAccessTokenId)
);
if (tokenDetailsInCache) {
return AccessTokenStatusSchema.parseAsync(JSON.parse(tokenDetailsInCache));
}
};
const updateServiceTokenStatus = async (serviceTokenId: string) => {
await keyStore.setItemWithExpiry(
KeyStorePrefixes.ServiceTokenStatusUpdate(serviceTokenId),
KeyStoreTtls.AccessTokenStatusUpdateInSeconds,
JSON.stringify({ lastUpdatedAt: new Date() })
);
await queueService.queue(
QueueName.AccessTokenStatusUpdate,
QueueJobs.ServiceTokenStatusUpdate,
{
serviceTokenId
},
{
delay: applyJitter(secondsToMillis(KeyStoreTtls.AccessTokenStatusUpdateInSeconds / 2), secondsToMillis(10)),
// https://docs.bullmq.io/guide/jobs/job-ids
jobId: KeyStorePrefixes.ServiceTokenStatusUpdate(serviceTokenId).replaceAll(":", "_"),
removeOnFail: true,
removeOnComplete: true
}
);
};
const updateIdentityAccessTokenStatus = async (identityAccessTokenId: string, numberOfUses: number) => {
await keyStore.setItemWithExpiry(
KeyStorePrefixes.IdentityAccessTokenStatusUpdate(identityAccessTokenId),
KeyStoreTtls.AccessTokenStatusUpdateInSeconds,
JSON.stringify({ lastUpdatedAt: new Date(), numberOfUses })
);
await queueService.queue(
QueueName.AccessTokenStatusUpdate,
QueueJobs.IdentityAccessTokenStatusUpdate,
{
identityAccessTokenId,
numberOfUses
},
{
delay: applyJitter(secondsToMillis(KeyStoreTtls.AccessTokenStatusUpdateInSeconds / 2), secondsToMillis(10)),
jobId: KeyStorePrefixes.IdentityAccessTokenStatusUpdate(identityAccessTokenId).replaceAll(":", "_"),
removeOnFail: true,
removeOnComplete: true
}
);
};
queueService.start(QueueName.AccessTokenStatusUpdate, async (job) => {
// for identity token update
if (job.name === QueueJobs.IdentityAccessTokenStatusUpdate && "identityAccessTokenId" in job.data) {
const { identityAccessTokenId } = job.data;
const tokenDetails = { lastUpdatedAt: new Date(job.timestamp), numberOfUses: job.data.numberOfUses };
const tokenDetailsInCache = await getIdentityTokenDetailsInCache(identityAccessTokenId);
if (tokenDetailsInCache) {
tokenDetails.numberOfUses = tokenDetailsInCache.numberOfUses;
tokenDetails.lastUpdatedAt = new Date(tokenDetailsInCache.lastUpdatedAt);
}
await identityAccessTokenDAL.updateById(identityAccessTokenId, {
accessTokenLastUsedAt: tokenDetails.lastUpdatedAt,
accessTokenNumUses: tokenDetails.numberOfUses
});
return;
}
// for service token
if (job.name === QueueJobs.ServiceTokenStatusUpdate && "serviceTokenId" in job.data) {
const { serviceTokenId } = job.data;
const tokenDetailsInCache = await keyStore.getItem(KeyStorePrefixes.ServiceTokenStatusUpdate(serviceTokenId));
let lastUsed = new Date(job.timestamp);
if (tokenDetailsInCache) {
const tokenDetails = await AccessTokenStatusSchema.pick({ lastUpdatedAt: true }).parseAsync(
JSON.parse(tokenDetailsInCache)
);
lastUsed = new Date(tokenDetails.lastUpdatedAt);
}
await serviceTokenDAL.updateById(serviceTokenId, {
lastUsed
});
}
});
queueService.listen(QueueName.AccessTokenStatusUpdate, "failed", (_, err) => {
logger.error(err, `${QueueName.AccessTokenStatusUpdate}: Failed to updated access token status`);
});
return { updateIdentityAccessTokenStatus, updateServiceTokenStatus, getIdentityTokenDetailsInCache };
};

@ -1,3 +1,5 @@
import { ProjectMembershipRole } from "@app/db/schemas";
export enum TokenType {
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
@ -49,3 +51,19 @@ export type TIssueAuthTokenDTO = {
ip: string;
userAgent: string;
};
export enum TokenMetadataType {
InviteToProjects = "projects-invite"
}
export type TTokenInviteToProjectsMetadataPayload = {
projectIds: string[];
projectRoleSlug: ProjectMembershipRole;
userId: string;
orgId: string;
};
export type TTokenMetadata = {
type: TokenMetadataType.InviteToProjects;
payload: TTokenInviteToProjectsMetadataPayload;
};

@ -583,7 +583,13 @@ export const authLoginServiceFactory = ({
} else {
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
if (isLinkingRequired) {
user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] });
// we update the names here because upon org invitation, the names are set to be NULL
// if user is signing up with SSO after invitation, their names should be set based on their SSO profile
user = await userDAL.updateById(user.id, {
authMethods: [...(user.authMethods || []), authMethod],
firstName: !user.isAccepted ? firstName : undefined,
lastName: !user.isAccepted ? lastName : undefined
});
}
}

@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@ -17,9 +17,12 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { addMembersToProject } from "../project-membership/project-membership-fns";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TAuthDALFactory } from "./auth-dal";
@ -32,10 +35,14 @@ type TAuthSignupDep = {
userDAL: TUserDALFactory;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
| "find"
| "transaction"
| "insertMany"
| "deletePendingUserGroupMembershipsByUserIds"
| "findUserGroupMembershipsInProject"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
@ -43,6 +50,8 @@ type TAuthSignupDep = {
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
};
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
@ -58,6 +67,8 @@ export const authSignupServiceFactory = ({
smtpService,
orgService,
orgDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
licenseService
}: TAuthSignupDep) => {
// first step of signup. create user and send email
@ -301,7 +312,8 @@ export const authSignupServiceFactory = ({
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
authorization
authorization,
tokenMetadata
}: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByUsername(email);
if (!user || (user && user.isAccepted)) {
@ -358,6 +370,45 @@ export const authSignupServiceFactory = ({
tx
);
if (tokenMetadata) {
const metadataObj = jwt.verify(tokenMetadata, appCfg.AUTH_SECRET) as TTokenMetadata;
if (
metadataObj?.payload?.userId !== user.id ||
metadataObj?.payload?.orgId !== orgMembership.orgId ||
metadataObj?.type !== TokenMetadataType.InviteToProjects
) {
throw new UnauthorizedError({
message: "Malformed or invalid metadata token"
});
}
for await (const projectId of metadataObj.payload.projectIds) {
await addMembersToProject({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}).addMembersToNonE2EEProject(
{
emails: [user.email!],
usernames: [],
projectId,
projectMembershipRole: metadataObj.payload.projectRoleSlug,
sendEmails: false
},
{
tx,
throwOnProjectNotFound: false
}
);
}
}
const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted },

@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
ip: string;
userAgent: string;
authorization: string;
tokenMetadata?: string;
};

@ -13,6 +13,13 @@ import {
TRebuildCaCrlDTO
} from "./certificate-authority-types";
/* eslint-disable no-bitwise */
export const createSerialNumber = () => {
const randomBytes = crypto.randomBytes(32);
randomBytes[0] &= 0x7f; // ensure the first bit is 0
return randomBytes.toString("hex");
};
export const createDistinguishedName = (parts: TDNParts) => {
const dnParts = [];
if (parts.country) dnParts.push(`C=${parts.country}`);
@ -284,12 +291,11 @@ export const rebuildCaCrl = async ({
thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => {
const revocationDate = new Date(revokedCert.revokedAt as Date);
return {
serialNumber: revokedCert.serialNumber,
revocationDate: new Date(revokedCert.revokedAt as Date),
reason: revokedCert.revocationReason as number,
invalidity: new Date("2022/01/01"),
issuer: ca.dn
revocationDate,
reason: revokedCert.revocationReason as number
};
}),
signingAlgorithm: alg,

@ -8,6 +8,7 @@ import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
@ -25,6 +26,7 @@ import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cer
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import {
createDistinguishedName,
createSerialNumber,
getCaCertChain, // TODO: consider rename
getCaCertChains,
getCaCredentials,
@ -147,7 +149,7 @@ export const certificateAuthorityServiceFactory = ({
? new Date(notAfter)
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const ca = await certificateAuthorityDAL.create(
{
@ -263,7 +265,8 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityCrlDAL.create(
{
caId: ca.id,
encryptedCrl
encryptedCrl,
caSecretId: caSecret.id
},
tx
);
@ -433,7 +436,7 @@ export const certificateAuthorityServiceFactory = ({
// get latest CA certificate
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@ -846,7 +849,7 @@ export const certificateAuthorityServiceFactory = ({
kmsService
});
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
@ -1142,7 +1145,7 @@ export const certificateAuthorityServiceFactory = ({
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const { caPrivateKey } = await getCaCredentials({
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
@ -1150,9 +1153,15 @@ export const certificateAuthorityServiceFactory = ({
kmsService
});
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`;
const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
@ -1203,7 +1212,7 @@ export const certificateAuthorityServiceFactory = ({
);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
@ -1286,24 +1295,23 @@ export const certificateAuthorityServiceFactory = ({
* Return new leaf certificate issued by CA with id [caId].
* Note: CSR is generated externally and submitted to Infisical.
*/
const signCertFromCa = async ({
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TSignCertFromCaDTO) => {
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
let ca: TCertificateAuthorities | undefined;
let certificateTemplate: TCertificateTemplates | undefined;
const {
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter
} = dto;
let collectionId = pkiCollectionId;
if (caId) {
@ -1324,15 +1332,20 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "CA not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
if (!dto.isInternal) {
const { permission } = await permissionService.getProjectPermission(
dto.actor,
dto.actorId,
ca.projectId,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Certificates
);
}
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
@ -1373,6 +1386,8 @@ export const certificateAuthorityServiceFactory = ({
notAfterDate = new Date(notAfter);
} else if (ttl) {
notAfterDate = new Date(new Date().getTime() + ms(ttl));
} else if (certificateTemplate?.ttl) {
notAfterDate = new Date(new Date().getTime() + ms(certificateTemplate.ttl));
}
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
@ -1417,6 +1432,7 @@ export const certificateAuthorityServiceFactory = ({
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
let altNamesFromCsr: string = "";
let altNamesArray: {
type: "email" | "dns";
value: string;
@ -1445,7 +1461,24 @@ export const certificateAuthorityServiceFactory = ({
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
});
} else {
// attempt to read from CSR if altNames is not explicitly provided
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns")
.map((name) => ({
type: name.type as "email" | "dns",
value: name.value
}));
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
}
}
if (altNamesArray.length) {
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
@ -1462,7 +1495,7 @@ export const certificateAuthorityServiceFactory = ({
);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
@ -1491,7 +1524,7 @@ export const certificateAuthorityServiceFactory = ({
status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject,
commonName: cn,
altNames,
altNames: altNamesFromCsr || altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate
@ -1529,7 +1562,7 @@ export const certificateAuthorityServiceFactory = ({
});
return {
certificate: leafCert.toString("pem"),
certificate: leafCert,
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,

@ -97,18 +97,33 @@ export type TIssueCertFromCaDTO = {
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = {
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO =
| {
isInternal: true;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames?: string;
ttl?: string;
notBefore?: string;
notAfter?: string;
}
| ({
isInternal: false;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">);
export type TDNParts = {
commonName?: string;

@ -0,0 +1,24 @@
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
const cmsSigned = new SignedData({
encapContentInfo: new EncapsulatedContentInfo({
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
}),
certificates: certs
});
const cmsContent = new ContentInfo({
contentType: "1.2.840.113549.1.7.2", // SignedData
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
content: cmsSigned.toSchema()
});
const derBuffer = cmsContent.toSchema().toBER(false);
const base64Pkcs7 = Buffer.from(derBuffer)
.toString("base64")
.replace(/(.{64})/g, "$1\n"); // we add a linebreak for CURL clients
return base64Pkcs7;
};

@ -0,0 +1,231 @@
import * as x509 from "@peculiar/x509";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = {
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
export const certificateEstServiceFactory = ({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
}: TCertificateEstServiceFactoryDep) => {
const simpleReenroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const cert = new x509.X509Certificate(leafCertificate);
// We have to assert that the client certificate provided can be traced back to the Root CA
const caCertChains = await getCaCertChains({
caId: certTemplate.caId,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const verifiedChains = await Promise.all(
caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate);
const caChain =
chain.certificateChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]);
})
);
if (!verifiedChains.some(Boolean)) {
throw new BadRequestError({
message: "Invalid client certificate: unable to build a valid certificate chain"
});
}
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
const csrObj = new x509.Pkcs10CertificateRequest(csr);
if (csrObj.subject !== cert.subject) {
throw new BadRequestError({
message: "Subject mismatch"
});
}
let csrSanSet: Set<string> = new Set();
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (csrSanExtension) {
const sanNames = new x509.GeneralNames(csrSanExtension.value);
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
let certSanSet: Set<string> = new Set();
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
if (certSanExtension) {
const sanNames = new x509.GeneralNames(certSanExtension.value);
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
throw new BadRequestError({
message: "Subject alternative names mismatch"
});
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const simpleEnroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
/* We first have to assert that the client certificate provided can be traced back to the attached
CA chain in the EST configuration
*/
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
/**
* Return the CA certificate and CA certificate chain for the CA bound to
* the certificate template with id [certificateTemplateId] as part of EST protocol
*/
const getCaCerts = async ({ certificateTemplateId }: { certificateTemplateId: string }) => {
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
if (!ca) {
throw new NotFoundError({
message: "Certificate Authority not found"
});
}
const { caCert, caCertChain } = await getCaCertChain({
caCertId: ca.activeCaCertId as string,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificates = caCertChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const caCertificate = new x509.X509Certificate(caCert);
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
};
return {
simpleEnroll,
simpleReenroll,
getCaCerts
};
};

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

@ -1,20 +1,35 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
import { TCertificateTemplateEstConfigDALFactory } from "./certificate-template-est-config-dal";
import {
TCreateCertTemplateDTO,
TCreateEstConfigurationDTO,
TDeleteCertTemplateDTO,
TGetCertTemplateDTO,
TUpdateCertTemplateDTO
TGetEstConfigurationDTO,
TUpdateCertTemplateDTO,
TUpdateEstConfigurationDTO
} from "./certificate-template-types";
type TCertificateTemplateServiceFactoryDep = {
certificateTemplateDAL: TCertificateTemplateDALFactory;
certificateTemplateEstConfigDAL: TCertificateTemplateEstConfigDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@ -23,8 +38,11 @@ export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTe
export const certificateTemplateServiceFactory = ({
certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL,
permissionService
permissionService,
kmsService,
projectDAL
}: TCertificateTemplateServiceFactoryDep) => {
const createCertTemplate = async ({
caId,
@ -187,10 +205,228 @@ export const certificateTemplateServiceFactory = ({
return certTemplate;
};
const createEstConfiguration = async ({
certificateTemplateId,
caChain,
passphrase,
isEnabled,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateEstConfigurationDTO) => {
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const appCfg = getConfig();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
// validate CA chain
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
const estConfig = await certificateTemplateEstConfigDAL.create({
certificateTemplateId,
hashedPassphrase,
encryptedCaChain,
isEnabled
});
return { ...estConfig, projectId: certTemplate.projectId };
};
const updateEstConfiguration = async ({
certificateTemplateId,
caChain,
passphrase,
isEnabled,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateEstConfigurationDTO) => {
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
certTemplate.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({
certificateTemplateId
});
if (!originalCaEstConfig) {
throw new NotFoundError({
message: "EST configuration not found"
});
}
const appCfg = getConfig();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
const updatedData: TCertificateTemplateEstConfigsUpdate = {
isEnabled
};
if (caChain) {
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
updatedData.encryptedCaChain = encryptedCaChain;
}
if (passphrase) {
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
updatedData.hashedPassphrase = hashedPassphrase;
}
const estConfig = await certificateTemplateEstConfigDAL.updateById(originalCaEstConfig.id, updatedData);
return { ...estConfig, projectId: certTemplate.projectId };
};
const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => {
const { certificateTemplateId } = dto;
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found."
});
}
if (!dto.isInternal) {
const { permission } = await permissionService.getProjectPermission(
dto.actor,
dto.actorId,
certTemplate.projectId,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
}
const estConfig = await certificateTemplateEstConfigDAL.findOne({
certificateTemplateId
});
if (!estConfig) {
throw new NotFoundError({
message: "EST configuration not found"
});
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaChain = await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
});
return {
certificateTemplateId,
id: estConfig.id,
isEnabled: estConfig.isEnabled,
caChain: decryptedCaChain.toString(),
hashedPassphrase: estConfig.hashedPassphrase,
projectId: certTemplate.projectId
};
};
return {
createCertTemplate,
getCertTemplate,
deleteCertTemplate,
updateCertTemplate
updateCertTemplate,
createEstConfiguration,
updateEstConfiguration,
getEstConfiguration
};
};

@ -26,3 +26,27 @@ export type TGetCertTemplateDTO = {
export type TDeleteCertTemplateDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateEstConfigurationDTO = {
certificateTemplateId: string;
caChain: string;
passphrase: string;
isEnabled: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateEstConfigurationDTO = {
certificateTemplateId: string;
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetEstConfigurationDTO =
| {
isInternal: true;
certificateTemplateId: string;
}
| ({
isInternal: false;
certificateTemplateId: string;
} & Omit<TProjectPermission, "projectId">);

@ -24,3 +24,19 @@ export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
return x509.X509CrlReason.unspecified;
}
};
export const isCertChainValid = async (certificates: x509.X509Certificate[]) => {
if (certificates.length === 1) {
return true;
}
const leafCert = certificates[0];
const chain = new x509.X509ChainBuilder({
certificates: certificates.slice(1)
});
const chainItems = await chain.build(leafCert);
// chain.build() implicitly verifies the chain
return chainItems.length === certificates.length;
};

@ -1,7 +1,7 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, sqlNestRelationships } from "@app/lib/knex";
@ -95,5 +95,107 @@ export const groupProjectDALFactory = (db: TDbClient) => {
}
};
return { ...groupProjectOrm, findByProjectId };
// The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId).
// We need to join the GroupProjectMembership table with the Groups table to get the group name and slug.
// We also need to join the GroupProjectMembershipRole table to get the role of the group in the project.
const findAllProjectGroupMembers = async (projectId: string) => {
const docs = await db(TableName.UserGroupMembership)
// Join the GroupProjectMembership table with the Groups table to get the group name and slug.
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership
)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.join<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership),
db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole),
db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole),
db.ref("name").as("projectName").withSchema(TableName.Project)
)
.where({ isGhost: false });
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({
isGroupMember: true,
id,
userId,
projectId,
project: {
id: projectId,
name: projectName
},
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
};
return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers };
};

@ -5,6 +5,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
@ -13,19 +14,24 @@ import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity
type TIdentityAccessTokenServiceFactoryDep = {
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
accessTokenQueue: Pick<
TAccessTokenQueueServiceFactory,
"updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache"
>;
};
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
export const identityAccessTokenServiceFactory = ({
identityAccessTokenDAL,
identityOrgMembershipDAL
identityOrgMembershipDAL,
accessTokenQueue
}: TIdentityAccessTokenServiceFactoryDep) => {
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
const {
id: tokenId,
accessTokenTTL,
accessTokenNumUses,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenLastRenewedAt,
createdAt: accessTokenCreatedAt
@ -83,7 +89,12 @@ export const identityAccessTokenServiceFactory = ({
});
if (!identityAccessToken) throw new UnauthorizedError();
await validateAccessTokenExp(identityAccessToken);
let { accessTokenNumUses } = identityAccessToken;
const tokenStatusInCache = await accessTokenQueue.getIdentityTokenDetailsInCache(identityAccessToken.id);
if (tokenStatusInCache) {
accessTokenNumUses = tokenStatusInCache.numberOfUses;
}
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
const { accessTokenMaxTTL, createdAt: accessTokenCreatedAt, accessTokenTTL } = identityAccessToken;
@ -164,14 +175,14 @@ export const identityAccessTokenServiceFactory = ({
throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
}
await validateAccessTokenExp(identityAccessToken);
let { accessTokenNumUses } = identityAccessToken;
const tokenStatusInCache = await accessTokenQueue.getIdentityTokenDetailsInCache(identityAccessToken.id);
if (tokenStatusInCache) {
accessTokenNumUses = tokenStatusInCache.numberOfUses;
}
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
await identityAccessTokenDAL.updateById(identityAccessToken.id, {
accessTokenLastUsedAt: new Date(),
$incr: {
accessTokenNumUses: 1
}
});
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
};

@ -65,7 +65,7 @@ export const identityAwsAuthServiceFactory = ({
}
}: { data: TGetCallerIdentityResponse } = await axios({
method: iamHttpRequestMethod,
url: identityAwsAuth.stsEndpoint,
url: headers?.Host ? `https://${headers.Host}` : identityAwsAuth.stsEndpoint,
headers,
data: body
});

@ -1030,11 +1030,31 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
return apps;
};
const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: string; orgName: string }) => {
const res = (
await request.get<{ count: number; value: Record<string, string>[] }>(
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/projects?api-version=7.2-preview.2`,
{
headers: {
Authorization: `Basic ${accessToken}`
}
}
)
).data;
const apps = res.value.map((a) => ({
name: a.name,
appId: a.id
}));
return apps;
};
export const getApps = async ({
integration,
accessToken,
accessId,
teamId,
azureDevOpsOrgName,
workspaceSlug,
url
}: {
@ -1042,6 +1062,7 @@ export const getApps = async ({
accessToken: string;
accessId?: string;
teamId?: string | null;
azureDevOpsOrgName?: string | null;
workspaceSlug?: string;
url?: string | null;
}): Promise<App[]> => {
@ -1184,6 +1205,12 @@ export const getApps = async ({
accessToken
});
case Integrations.AZURE_DEVOPS:
return getAppsAzureDevOps({
accessToken,
orgName: azureDevOpsOrgName as string
});
default:
throw new BadRequestError({ message: "integration not found" });
}

@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({
actorOrgId,
actorAuthMethod,
teamId,
azureDevOpsOrgName,
id,
workspaceSlug
}: TIntegrationAuthAppsDTO) => {
@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({
accessToken,
accessId,
teamId,
azureDevOpsOrgName,
workspaceSlug,
url: integrationAuth.url
});

@ -1,3 +1,4 @@
import { TIntegrations } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export type TGetIntegrationAuthDTO = {
@ -28,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
export type TIntegrationAuthAppsDTO = {
id: string;
teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string;
} & Omit<TProjectPermission, "projectId">;
@ -163,3 +165,13 @@ export type TTeamCityBuildConfig = {
href: string;
webUrl: string;
};
export type TIntegrationsWithEnvironment = TIntegrations & {
environment?:
| {
id?: string | null | undefined;
name?: string | null | undefined;
}
| null
| undefined;
};

@ -31,7 +31,8 @@ export enum Integrations {
CLOUD_66 = "cloud-66",
NORTHFLANK = "northflank",
HASURA_CLOUD = "hasura-cloud",
RUNDECK = "rundeck"
RUNDECK = "rundeck",
AZURE_DEVOPS = "azure-devops"
}
export enum IntegrationType {
@ -88,6 +89,7 @@ export enum IntegrationUrls {
CLOUD_66_API_URL = "https://app.cloud66.com/api",
NORTHFLANK_API_URL = "https://api.northflank.com",
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
AZURE_DEVOPS_API_URL = "https://dev.azure.com",
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Azure DevOps",
slug: "azure-devops",
image: "Microsoft Azure.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
}
];

@ -35,6 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
@ -2075,6 +2076,116 @@ const syncSecretsTravisCI = async ({
}
};
/**
* Sync/push [secrets] to GitLab repo with name [integration.app]
*/
const syncSecretsAzureDevops = async ({
integrationAuth,
integration,
secrets,
accessToken
}: {
integrationAuth: TIntegrationAuths;
integration: TIntegrationsWithEnvironment;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
if (!integration.appId || !integration.app) {
throw new Error("Azure DevOps: orgId and projectId are required");
}
if (!integration.environment || !integration.environment.name) {
throw new Error("Azure DevOps: environment is required");
}
const headers = {
Authorization: `Basic ${accessToken}`
};
const azureDevopsApiUrl = integrationAuth.url ? `${integrationAuth.url}` : IntegrationUrls.AZURE_DEVOPS_API_URL;
const getEnvGroupId = async (orgId: string, project: string, env: string) => {
let groupId;
const url: string | null =
`${azureDevopsApiUrl}/${orgId}/${project}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
const response = await request.get(url, { headers });
for (const group of response.data.value) {
const groupName = group.name;
if (groupName === env) {
groupId = group.id;
return { groupId, groupName };
}
}
return { groupId: "", groupName: "" };
};
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
const variables: Record<string, { value: string }> = {};
for (const key of Object.keys(secrets)) {
variables[key] = { value: secrets[key].value };
}
if (!groupId) {
// create new variable group if not present
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
const config = {
method: "POST",
url,
data: {
name: integration.environment.name,
description: integration.environment.name,
type: "Vsts",
owner: "Library",
variables,
variableGroupProjectReferences: [
{
name: integration.environment.name,
projectReference: {
name: integration.appId
}
}
]
},
headers: {
headers
}
};
const res = await request.post(url, config.data, config.headers);
if (res.status !== 200) {
throw new Error(`Azure DevOps: Failed to create variable group: ${res.statusText}`);
}
} else {
// sync variables for pre-existing variable group
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.2-preview.2`;
const config = {
method: "PUT",
url,
data: {
name: groupName,
description: groupName,
type: "Vsts",
owner: "Library",
variables,
variableGroupProjectReferences: [
{
name: groupName,
projectReference: {
name: integration.appId
}
}
]
},
headers: {
headers
}
};
const res = await request.put(url, config.data, config.headers);
if (res.status !== 200) {
throw new Error(`Azure DevOps: Failed to update variable group: ${res.statusText}`);
}
}
};
/**
* Sync/push [secrets] to GitLab repo with name [integration.app]
*/
@ -3714,6 +3825,15 @@ export const syncIntegrationSecrets = async ({
updateManySecretsRawFn
});
break;
case Integrations.AZURE_DEVOPS:
await syncSecretsAzureDevops({
integrationAuth,
integration,
secrets,
accessToken
});
break;
case Integrations.AWS_PARAMETER_STORE:
response = await syncSecretsAWSParameterStore({
integration,

@ -114,10 +114,11 @@ export const orgDALFactory = (db: TDbClient) => {
}
};
const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => {
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
try {
const members = await db
.replicaNode()(TableName.OrgMembership)
const conn = tx || db;
const members = await conn(TableName.OrgMembership)
// .replicaNode()(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>(
@ -126,18 +127,18 @@ export const orgDALFactory = (db: TDbClient) => {
`${TableName.Users}.id`
)
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
conn.ref("id").withSchema(TableName.OrgMembership),
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
conn.ref("orgId").withSchema(TableName.OrgMembership),
conn.ref("role").withSchema(TableName.OrgMembership),
conn.ref("roleId").withSchema(TableName.OrgMembership),
conn.ref("status").withSchema(TableName.OrgMembership),
conn.ref("username").withSchema(TableName.Users),
conn.ref("email").withSchema(TableName.Users),
conn.ref("firstName").withSchema(TableName.Users),
conn.ref("lastName").withSchema(TableName.Users),
conn.ref("id").withSchema(TableName.Users).as("userId"),
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ isGhost: false })
.whereIn("username", usernames);

@ -1,4 +1,5 @@
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
@ -12,6 +13,7 @@ type TDeleteOrgMembership = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
};
export const deleteOrgMembershipFn = async ({
@ -19,6 +21,7 @@ export const deleteOrgMembershipFn = async ({
orgId,
orgDAL,
projectMembershipDAL,
projectUserAdditionalPrivilegeDAL,
projectKeyDAL,
userAliasDAL,
licenseService
@ -39,6 +42,13 @@ export const deleteOrgMembershipFn = async ({
tx
);
await projectUserAdditionalPrivilegeDAL.delete(
{
userId: orgMembership.userId
},
tx
);
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);

@ -4,12 +4,21 @@ import crypto from "crypto";
import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
import {
OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectVersion,
TableName,
TUsers
} from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
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 { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
@ -23,10 +32,14 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal";
import { verifyProjectVersions } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { addMembersToProject } from "../project-membership/project-membership-fns";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@ -55,8 +68,11 @@ type TOrgServiceFactoryDep = {
userDAL: TUserDALFactory;
groupDAL: TGroupDALFactory;
projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
projectMembershipDAL: Pick<
TProjectMembershipDALFactory,
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
@ -67,6 +83,10 @@ type TOrgServiceFactoryDep = {
TLicenseServiceFactory,
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
>;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
};
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@ -84,10 +104,14 @@ export const orgServiceFactory = ({
projectMembershipDAL,
projectKeyDAL,
orgMembershipDAL,
projectUserAdditionalPrivilegeDAL,
tokenService,
orgBotDAL,
licenseService,
samlConfigDAL
samlConfigDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL
}: TOrgServiceFactoryDep) => {
/*
* Get organization details by the organization id
@ -417,10 +441,15 @@ export const orgServiceFactory = ({
const inviteUserToOrganization = async ({
orgId,
userId,
inviteeEmail,
inviteeEmails,
organizationRoleSlug,
projectRoleSlug,
projectIds,
actorAuthMethod,
actorOrgId
}: TInviteUserToOrgDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
@ -447,98 +476,203 @@ export const orgServiceFactory = ({
});
}
const invitee = await orgDAL.transaction(async (tx) => {
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
if (inviteeUser) {
// if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore
const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
},
{ tx }
);
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({
message: "Failed to invite an existing member of org",
name: "Invite user to org"
});
if (projectIds?.length) {
const projects = await projectDAL.find({
orgId,
$in: {
id: projectIds
}
});
if (!inviteeMembership) {
await orgDAL.createMembership(
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
}
return inviteeUser;
}
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
if (isEmailInvalid) {
// if its not v3, throw an error
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
throw new BadRequestError({
message: "Provided a disposable email",
name: "Org invite"
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
});
}
// not invited before
const user = await userDAL.create(
{
username: inviteeEmail,
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
await orgDAL.createMembership(
{
inviteEmail: inviteeEmail,
orgId,
userId: user.id,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
return user;
});
}
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: invitee.id,
orgId
const inviteeUsers = await orgDAL.transaction(async (tx) => {
const users: Pick<
TUsers & { orgId: string },
"id" | "firstName" | "lastName" | "email" | "orgId" | "username"
>[] = [];
for await (const inviteeEmail of inviteeEmails) {
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
if (inviteeUser) {
// if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore
const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
},
{ tx }
);
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({
message: `Failed to invite members because ${inviteeEmail} is already part of the organization`,
name: "Invite user to org"
});
}
if (!inviteeMembership) {
await orgDAL.createMembership(
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
if (projectIds?.length) {
if (
organizationRoleSlug === OrgMembershipRole.Custom ||
projectRoleSlug === ProjectMembershipRole.Custom
) {
throw new BadRequestError({
message: "Custom roles are not supported for inviting users to projects and organizations"
});
}
if (!projectRoleSlug) {
throw new BadRequestError({
message: "Selecting a project role is required to invite users to projects"
});
}
await projectMembershipDAL.insertMany(
projectIds.map((id) => ({ projectId: id, userId: inviteeUser.id })),
tx
);
for await (const projectId of projectIds) {
await addMembersToProject({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}).addMembersToNonE2EEProject(
{
emails: [inviteeEmail],
usernames: [],
projectId,
projectMembershipRole: projectRoleSlug,
sendEmails: false
},
{
tx
}
);
}
}
}
return [{ ...inviteeUser, orgId }];
}
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
if (isEmailInvalid) {
throw new BadRequestError({
message: "Provided a disposable email",
name: "Org invite"
});
}
// not invited before
const user = await userDAL.create(
{
username: inviteeEmail,
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
await orgDAL.createMembership(
{
inviteEmail: inviteeEmail,
orgId,
userId: user.id,
role: organizationRoleSlug,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
users.push({
...user,
orgId
});
}
return users;
});
const user = await userDAL.findById(userId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: user.firstName,
inviterUsername: user.username,
organizationName: org?.name,
email: inviteeEmail,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
const signupTokens: { email: string; link: string }[] = [];
if (inviteeUsers) {
for await (const invitee of inviteeUsers) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: invitee.id,
orgId
});
let inviteMetadata: string = "";
if (projectIds && projectIds?.length > 0) {
inviteMetadata = jwt.sign(
{
type: TokenMetadataType.InviteToProjects,
payload: {
projectIds,
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
userId: invitee.id,
orgId
}
} satisfies TTokenMetadata,
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_INVITE_LIFETIME
}
);
}
signupTokens.push({
email: invitee.email || invitee.username,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
});
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [invitee.email || invitee.username],
substitutions: {
metadata: inviteMetadata,
inviterFirstName: user.firstName,
inviterUsername: user.username,
organizationName: org?.name,
email: invitee.email || invitee.username,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
}
}
await licenseService.updateSubscriptionOrgMemberCount(orgId);
if (!appCfg.isSmtpConfigured) {
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
return signupTokens;
}
};
@ -632,6 +766,7 @@ export const orgServiceFactory = ({
orgId,
orgDAL,
projectMembershipDAL,
projectUserAdditionalPrivilegeDAL,
projectKeyDAL,
userAliasDAL,
licenseService

@ -1,3 +1,4 @@
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@ -29,7 +30,10 @@ export type TInviteUserToOrgDTO = {
orgId: string;
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
inviteeEmail: string;
inviteeEmails: string[];
organizationRoleSlug: OrgMembershipRole;
projectIds?: string[];
projectRoleSlug?: ProjectMembershipRole;
};
export type TVerifyUserToOrgDTO = {

@ -0,0 +1,190 @@
import { Knex } from "knex";
import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
type TAddMembersToProjectArg = {
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectById" | "findProjectGhostUser">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "insertMany">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">;
};
type AddMembersToNonE2EEProjectDTO = {
emails: string[];
usernames: string[];
projectId: string;
projectMembershipRole: ProjectMembershipRole;
sendEmails?: boolean;
};
type AddMembersToNonE2EEProjectOptions = {
tx?: Knex;
throwOnProjectNotFound?: boolean;
};
export const addMembersToProject = ({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
userGroupMembershipDAL,
projectUserMembershipRoleDAL,
smtpService
}: TAddMembersToProjectArg) => {
// Can create multiple memberships for a singular project, based on user email / username
const addMembersToNonE2EEProject = async (
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
) => {
const processTransaction = async (tx: Knex) => {
const usernamesAndEmails = [...emails, ...usernames];
const project = await projectDAL.findProjectById(projectId);
if (!project) {
if (options.throwOnProjectNotFound) {
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
}
return [];
}
const orgMembers = await orgDAL.findOrgMembersByUsername(
project.orgId,
[...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))],
tx
);
if (orgMembers.length !== usernamesAndEmails.length)
throw new BadRequestError({ message: "Some users are not part of org" });
if (!orgMembers.length) return [];
const existingMembers = await projectMembershipDAL.find({
projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: orgMembers.map((membership) => ({
orgMembershipId: membership.id,
projectMembershipRole,
userPublicKey: membership.user.publicKey
}))
});
const members: TProjectMemberships[] = [];
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
tx
);
members.push(...projectMemberships);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
if (sendEmails) {
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
const appCfg = getConfig();
if (recipients.length) {
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
}
return members;
};
if (options.tx) {
return processTransaction(options.tx);
}
return projectMembershipDAL.transaction(processTransaction);
};
return {
addMembersToNonE2EEProject
};
};

@ -2,32 +2,27 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import {
ProjectMembershipRole,
ProjectVersion,
SecretKeyEncoding,
TableName,
TProjectMemberships
} from "@app/db/schemas";
import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import { addMembersToProject } from "./project-membership-fns";
import {
ProjectUserMembershipTemporaryMode,
TAddUsersToWorkspaceDTO,
@ -51,9 +46,11 @@ type TProjectMembershipServiceFactoryDep = {
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
groupProjectDAL: TGroupProjectDALFactory;
};
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
@ -66,8 +63,10 @@ export const projectMembershipServiceFactory = ({
projectRoleDAL,
projectBotDAL,
orgDAL,
projectUserAdditionalPrivilegeDAL,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectDAL,
projectKeyDAL,
licenseService
@ -77,6 +76,7 @@ export const projectMembershipServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
includeGroupMembers,
projectId
}: TGetProjectMembershipDTO) => {
const { permission } = await permissionService.getProjectPermission(
@ -88,7 +88,25 @@ export const projectMembershipServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return projectMembershipDAL.findAllProjectMembers(projectId);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
// projectMembers[0].project
if (includeGroupMembers) {
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
const allMembers = [
...projectMembers.map((m) => ({ ...m, isGroupMember: false })),
...groupMembers.map((m) => ({ ...m, isGroupMember: true }))
];
// Ensure the userId is unique
const membersIds = new Set(allMembers.map((entity) => entity.user.id));
const uniqueMembers = allMembers.filter((entity) => membersIds.has(entity.user.id));
return uniqueMembers;
}
return projectMembers.map((m) => ({ ...m, isGroupMember: false }));
};
const getProjectMembershipByUsername = async ({
@ -222,116 +240,23 @@ export const projectMembershipServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const usernamesAndEmails = [...emails, ...usernames];
const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [
...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))
]);
if (orgMembers.length !== usernamesAndEmails.length)
throw new BadRequestError({ message: "Some users are not part of org" });
if (!orgMembers.length) return [];
const existingMembers = await projectMembershipDAL.find({
const members = await addMembersToProject({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}).addMembersToNonE2EEProject({
emails,
usernames,
projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
projectMembershipRole: ProjectMembershipRole.Member,
sendEmails
});
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: orgMembers.map((membership) => ({
orgMembershipId: membership.id,
projectMembershipRole: ProjectMembershipRole.Member,
userPublicKey: membership.user.publicKey
}))
});
const members: TProjectMemberships[] = [];
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
tx
);
members.push(...projectMemberships);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
});
if (sendEmails) {
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
const appCfg = getConfig();
if (recipients.length) {
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
}
return members;
};
@ -502,6 +427,16 @@ export const projectMembershipServiceFactory = ({
);
const memberships = await projectMembershipDAL.transaction(async (tx) => {
await projectUserAdditionalPrivilegeDAL.delete(
{
projectId,
$in: {
userId: projectMembers.map((membership) => membership.user.id)
}
},
tx
);
const deletedMemberships = await projectMembershipDAL.delete(
{
projectId,
@ -564,12 +499,25 @@ export const projectMembershipServiceFactory = ({
});
}
const deletedMembership = (
await projectMembershipDAL.delete({
projectId: project.id,
userId: actorId
})
)?.[0];
const deletedMembership = await projectMembershipDAL.transaction(async (tx) => {
await projectUserAdditionalPrivilegeDAL.delete(
{
projectId: project.id,
userId: actorId
},
tx
);
const membership = (
await projectMembershipDAL.delete(
{
projectId: project.id,
userId: actorId
},
tx
)
)?.[0];
return membership;
});
if (!deletedMembership) {
throw new BadRequestError({ message: "Failed to leave project" });

@ -1,6 +1,6 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetProjectMembershipDTO = TProjectPermission;
export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean } & TProjectPermission;
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
export enum ProjectUserMembershipTemporaryMode {
Relative = "relative"

@ -0,0 +1,52 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission
} from "@app/ee/services/permission/project-permission";
export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
return [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
permissions: projectAdminPermissions,
description: "Full administrative access over a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
permissions: projectMemberPermissions,
description: "Limited read/write role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
permissions: projectViewerPermission,
description: "Only read role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
projectId,
name: "No Access",
slug: ProjectMembershipRole.NoAccess,
permissions: projectNoAccessPermissions,
description: "No access to any resources in the project",
createdAt: new Date(),
updatedAt: new Date()
}
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
};

@ -5,13 +5,9 @@ import { ProjectMembershipRole } from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
ProjectPermissionActions,
ProjectPermissionSet,
ProjectPermissionSub,
projectViewerPermission
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "./project-role-dal";
import { getPredefinedRoles } from "./project-role-fns";
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
type TProjectRoleServiceFactoryDep = {
@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
return [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
permissions: projectAdminPermissions,
description: "Full administrative access over a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
permissions: projectMemberPermissions,
description: "Limited read/write role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
permissions: projectViewerPermission,
description: "Only read role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
projectId,
name: "No Access",
slug: ProjectMembershipRole.NoAccess,
permissions: projectNoAccessPermissions,
description: "No access to any resources in the project",
createdAt: new Date(),
updatedAt: new Date()
}
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
};
export const projectRoleServiceFactory = ({
projectRoleDAL,
permissionService,

@ -279,6 +279,34 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findProjectWithOrg = async (projectId: string) => {
// we just need the project, and we need to include a new .organization field that includes the org from the orgId reference
const project = await db(TableName.Project)
.where({ [`${TableName.Project}.id` as "id"]: projectId })
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.select(
db.ref("id").withSchema(TableName.Organization).as("organizationId"),
db.ref("name").withSchema(TableName.Organization).as("organizationName")
)
.select(selectAllTableCols(TableName.Project))
.first();
if (!project) {
throw new BadRequestError({ message: "Project not found" });
}
return {
...ProjectsSchema.parse(project),
organization: {
id: project.organizationId,
name: project.organizationName
}
};
};
return {
...projectOrm,
findAllProjects,
@ -288,6 +316,7 @@ export const projectDALFactory = (db: TDbClient) => {
findProjectById,
findProjectByFilter,
findProjectBySlug,
findProjectWithOrg,
checkProjectUpgradeStatus
};
};

@ -1,5 +1,6 @@
import crypto from "crypto";
import { ProjectVersion, TProjects } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
};
export const verifyProjectVersions = (projects: Pick<TProjects, "version">[], version: ProjectVersion) => {
for (const project of projects) {
if (project.version !== version) {
return false;
}
}
return true;
};
export const getProjectKmsCertificateKeyId = async ({
projectId,
projectDAL,

@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
@ -30,6 +31,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { getPredefinedRoles } from "../project-role/project-role-fns";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal";
@ -44,6 +47,7 @@ import {
TListProjectCasDTO,
TListProjectCertificateTemplatesDTO,
TListProjectCertsDTO,
TListProjectsDTO,
TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO,
@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
@ -112,6 +117,7 @@ export const projectServiceFactory = ({
projectEnvDAL,
licenseService,
projectUserMembershipRoleDAL,
projectRoleDAL,
identityProjectMembershipRoleDAL,
certificateAuthorityDAL,
certificateDAL,
@ -389,8 +395,34 @@ export const projectServiceFactory = ({
return deletedProject;
};
const getProjects = async (actorId: string) => {
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
const workspaces = await projectDAL.findAllProjects(actorId);
if (includeRoles) {
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const customRoles = await projectRoleDAL.find({
$in: {
projectId: workspaces.map((workspace) => workspace.id)
}
});
const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId);
const workspacesWithRoles = await Promise.all(
workspaces.map(async (workspace) => {
return {
...workspace,
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
};
})
);
return workspacesWithRoles;
}
return workspaces;
};

@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
actorOrgId: string | undefined;
} & Omit<TProjectPermission, "projectId">;
export type TListProjectsDTO = {
includeRoles: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpgradeProjectDTO = {
userPrivateKey: string;
} & TProjectPermission;

@ -8,6 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@ -26,6 +27,7 @@ type TServiceTokenServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
projectDAL: Pick<TProjectDALFactory, "findById">;
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
};
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
@ -35,7 +37,8 @@ export const serviceTokenServiceFactory = ({
userDAL,
permissionService,
projectEnvDAL,
projectDAL
projectDAL,
accessTokenQueue
}: TServiceTokenServiceFactoryDep) => {
const createServiceToken = async ({
iv,
@ -166,11 +169,9 @@ export const serviceTokenServiceFactory = ({
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash);
if (!isMatch) throw new UnauthorizedError();
const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, {
lastUsed: new Date()
});
await accessTokenQueue.updateServiceTokenStatus(serviceToken.id);
return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId };
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
};
return {

@ -25,6 +25,7 @@ export enum SmtpTemplates {
UnlockAccount = "unlockAccount.handlebars",
AccessApprovalRequest = "accessApprovalRequest.handlebars",
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview.handlebars",
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",

@ -9,7 +9,7 @@
<body>
<h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>

@ -0,0 +1,22 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Change Approval Request</title>
</head>
<body>
<h2>Hi {{firstName}},</h2>
<h2>New secret change requests are pending review.</h2>
<br />
<p>You have a secret change request pending your review in project "{{projectName}}", in the "{{organizationName}}"
organization.</p>
<p>
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
</body>
</html>

@ -100,7 +100,9 @@ export type TIntegrationCreatedEvent = {
export type TUserOrgInvitedEvent = {
event: PostHogEventTypes.UserOrgInvitation;
properties: {
inviteeEmail: string;
inviteeEmails: string[];
projectIds?: string[];
organizationRoleSlug?: string;
};
};

@ -8,6 +8,7 @@ import (
"encoding/hex"
"encoding/json"
"os"
"slices"
"strings"
"time"
@ -152,6 +153,28 @@ var loginCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
if err != nil {
util.HandleError(err)
}
if clearSelfHostedDomains {
infisicalConfig, err := util.GetConfigFile()
if err != nil {
util.HandleError(err)
}
infisicalConfig.Domains = []string{}
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
util.HandleError(err)
}
fmt.Println("Cleared all self-hosted domains from the config file")
return
}
infisicalClient := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
@ -464,6 +487,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
func init() {
rootCmd.AddCommand(loginCmd)
loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file")
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
@ -499,10 +523,12 @@ func DomainOverridePrompt() (bool, error) {
}
func askForDomain() error {
//query user to choose between Infisical cloud or self hosting
// query user to choose between Infisical cloud or self hosting
const (
INFISICAL_CLOUD = "Infisical Cloud"
SELF_HOSTING = "Self Hosting"
ADD_NEW_DOMAIN = "Add a new domain"
)
options := []string{INFISICAL_CLOUD, SELF_HOSTING}
@ -524,6 +550,36 @@ func askForDomain() error {
return nil
}
infisicalConfig, err := util.GetConfigFile()
if err != nil {
return fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err)
}
if infisicalConfig.Domains != nil && len(infisicalConfig.Domains) > 0 {
// If domains are present in the config, let the user select from the list or select to add a new domain
items := append(infisicalConfig.Domains, ADD_NEW_DOMAIN)
prompt := promptui.Select{
Label: "Which domain would you like to use?",
Items: items,
Size: 5,
}
_, selectedOption, err := prompt.Run()
if err != nil {
return err
}
if selectedOption != ADD_NEW_DOMAIN {
config.INFISICAL_URL = fmt.Sprintf("%s/api", selectedOption)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", selectedOption)
return nil
}
}
urlValidation := func(input string) error {
_, err := url.ParseRequestURI(input)
if err != nil {
@ -542,12 +598,23 @@ func askForDomain() error {
if err != nil {
return err
}
//trimmed the '/' from the end of the self hosting url
// Trimmed the '/' from the end of the self hosting url, and set the api & login url
domain = strings.TrimRight(domain, "/")
//set api and login url
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain)
//return nil
// Write the new domain to the config file, to allow the user to select it in the future if needed
// First check if infiscialConfig.Domains already includes the domain, if it does, do not add it again
if !slices.Contains(infisicalConfig.Domains, domain) {
infisicalConfig.Domains = append(infisicalConfig.Domains, domain)
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
return fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err)
}
}
return nil
}

@ -16,6 +16,7 @@ type ConfigFile struct {
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
Domains []string `json:"domains,omitempty"`
}
type LoggedInUser struct {

@ -7,6 +7,7 @@ services:
restart: always
ports:
- 8080:80
- 8443:443
volumes:
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:

@ -1,4 +1,4 @@
---
title: "Retrieve CRL"
openapi: "GET /api/v1/pki/ca/{caId}/crl"
title: "List CRLs"
openapi: "GET /api/v1/pki/ca/{caId}/crls"
---

@ -151,18 +151,24 @@ In the following steps, we explore how to revoke a X.509 certificate under a CA
</Step>
<Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it
against the CRL of a CA by selecting the **View CRL** option under the
issuing CA and downloading the CRL file.
against the CRL of a CA by heading to its Issuing CA and downloading the CRL.
![pki view crl](/images/platform/pki/ca-crl.png)
![pki download crl](/images/platform/pki/ca-crl-modal.png)
To verify a certificate against the
downloaded CRL with OpenSSL, you can use the following command:
```bash
openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem
```
Note that you can also obtain the CRL from the certificate itself by
referencing the CRL distribution point extension on the certificate itself.
To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command:
```bash
openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem
```
</Step>
@ -197,21 +203,25 @@ openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem
</Step>
<Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA.
To obtain the CRL of the CA, make an API request to the [Get CRL](/api-reference/endpoints/certificate-authorities/crl) API endpoint.
To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crls) API endpoint.
### Sample request
```bash Request
curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/crl' \
curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/crls' \
--header 'Authorization: Bearer <access-token>'
```
### Sample response
```bash Response
{
crl: "..."
}
[
{
id: "...",
crl: "..."
},
...
]
```
To verify a certificate against the CRL with OpenSSL, you can use the following command:

@ -0,0 +1,57 @@
---
title: "Enrollment over Secure Transport (EST)"
sidebarTitle: "Enrollment over Secure Transport (EST)"
description: "Learn how to manage certificate enrollment of clients using EST"
---
## Concept
Enrollment over Secure Transport (EST) is a protocol used to automate the secure provisioning of digital certificates for devices and applications over a secure HTTPS connection. It is primarily used when a client device needs to obtain or renew a certificate from a Certificate Authority (CA) on Infisical in a secure and standardized manner. EST is commonly employed in environments requiring strong authentication and encrypted communication, such as in IoT, enterprise networks, and secure web services.
Infisical's EST service is based on [RFC 7030](https://datatracker.ietf.org/doc/html/rfc7030) and implements the following endpoints:
- **cacerts** - provides the necessary CA chain for the client to validate certificates issued by the CA.
- **simpleenroll** - allows an EST client to request a new certificate from Infisical's EST server
- **simplereenroll** - similar to the /simpleenroll endpoint but is used for renewing an existing certificate.
These endpoints are exposed on port 8443 under the .well-known/est path e.g.
`https://app.infisical.com:8443/.well-known/est/estLabel/cacerts`
## Prerequisites
- You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca).
- The client devices need to have a bootstrap/pre-installed certificate.
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS.
## Guide to configuring EST
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates).
2. Proceed to the certificate template's enrollment settings
![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png)
3. Select **EST** as the client enrollment method and fill up the remaining fields.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png)
- **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server.
- **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password.
For security reasons, Infisical authenticates EST clients using both client certificate and passphrase.
4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png)
For demonstration, the complete URL of the supported EST endpoints will look like the following:
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simplereenroll
## Setting up EST clients
- To use the EST passphrase in your clients, configure it as the EST password. The EST username can be set to any arbitrary value.
- Use the appropriate client certificates for invoking the EST endpoints.
- For `simpleenroll`, use the bootstrapped/manufacturer client certificate.
- For `simplereenroll`, use a valid EST-issued client certificate.
- When configuring the PKCS#12 objects for the client certificates, only include the leaf certificate and the private key.

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