mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
169 Commits
daniel/sdk
...
daniel/env
Author | SHA1 | Date | |
---|---|---|---|
|
b9071ab2b3 | ||
|
bfab270d68 | ||
|
8ea6a1f3d5 | ||
|
828644799f | ||
|
411e67ae41 | ||
|
4914bc4b5a | ||
|
d7050a1947 | ||
|
3c59422511 | ||
|
c81204e6d5 | ||
|
880f39519f | ||
|
8646f6c50b | ||
|
437a9e6ccb | ||
|
b54139bd37 | ||
|
8a6a36ac54 | ||
|
c6eb973da0 | ||
|
21750a8c20 | ||
|
a598665b2f | ||
|
56bbf502a2 | ||
|
9975f7d83f | ||
|
7ad366b363 | ||
|
cca4d68d94 | ||
|
b82b94db54 | ||
|
de9cb265e0 | ||
|
7ac4ad3194 | ||
|
3ab6eb62c8 | ||
|
79680b6a73 | ||
|
58838c541f | ||
|
03cc71cfed | ||
|
02529106c9 | ||
|
d939ff289d | ||
|
d1816c3051 | ||
|
cb350788c0 | ||
|
cd58768d6f | ||
|
dcd6f4d55d | ||
|
3c828614b8 | ||
|
09e7988596 | ||
|
f40df19334 | ||
|
76c9d3488b | ||
|
0809da33e0 | ||
|
b528eec4bb | ||
|
5179103680 | ||
|
25a9e5f58a | ||
|
8ddfe7b6e9 | ||
|
c23f21d57a | ||
|
1242a43d98 | ||
|
1655ca27d1 | ||
|
2bcead03b0 | ||
|
41ab1972ce | ||
|
b00fff6922 | ||
|
97b01ca5f8 | ||
|
c2bd6f5ef3 | ||
|
18efc9a6de | ||
|
436ccb25fb | ||
|
8f08a352dd | ||
|
00f86cfd00 | ||
|
3944aafb11 | ||
|
a6b852fab9 | ||
|
2a043afe11 | ||
|
df8f2cf9ab | ||
|
a18015b1e5 | ||
|
8b80622d2f | ||
|
c0fd0a56f3 | ||
|
326764dd41 | ||
|
1f24d02c5e | ||
|
c130fbddd9 | ||
|
f560534493 | ||
|
10a97f4522 | ||
|
7a2f0214f3 | ||
|
a2b994ab23 | ||
|
c4715124dc | ||
|
67c1cb9bf1 | ||
|
68b1984a76 | ||
|
ba45e83880 | ||
|
28ecc37163 | ||
|
a6a2e2bae0 | ||
|
d8bbfacae0 | ||
|
58549c398f | ||
|
842ed62bec | ||
|
06d8800ee0 | ||
|
2ecfd1bb7e | ||
|
783d4c7bd6 | ||
|
fbf3f26abd | ||
|
1d09693041 | ||
|
626e37e3d0 | ||
|
07fd67b328 | ||
|
3f1f018adc | ||
|
fe04e6d20c | ||
|
d7171a1617 | ||
|
384a0daa31 | ||
|
c5c949e034 | ||
|
c2c9edf156 | ||
|
c8248ef4e9 | ||
|
9f6a6a7b7c | ||
|
121b642d50 | ||
|
59b16f647e | ||
|
2ab5932693 | ||
|
8dfcef3900 | ||
|
8ca70eec44 | ||
|
60df59c7f0 | ||
|
e231c531a6 | ||
|
d48bb910fa | ||
|
1317266415 | ||
|
f0938330a7 | ||
|
e1bb0ac3ad | ||
|
f54d930de2 | ||
|
288f47f4bd | ||
|
b090ebfd41 | ||
|
67773bff5e | ||
|
8ef1cfda04 | ||
|
2a79d5ba36 | ||
|
0cb95f36ff | ||
|
4a1dfda41f | ||
|
c238b7b6ae | ||
|
288d7e88ae | ||
|
83d314ba32 | ||
|
b94a0ffa6c | ||
|
f88389bf9e | ||
|
2e88c5e2c5 | ||
|
73f3b8173e | ||
|
b60e404243 | ||
|
aa5b88ff04 | ||
|
b7caff88cf | ||
|
10120e1825 | ||
|
31e66c18e7 | ||
|
fb06f5a3bc | ||
|
1515dd8a71 | ||
|
da18a12648 | ||
|
49a0d3cec6 | ||
|
e821a11271 | ||
|
af4428acec | ||
|
61370cc6b2 | ||
|
cf3b2ebbca | ||
|
e970cc0f47 | ||
|
bd5cd03aeb | ||
|
760a1e917a | ||
|
c46e4d7fc1 | ||
|
1f3896231a | ||
|
4323f6fa8f | ||
|
65db91d491 | ||
|
ae5b57f69f | ||
|
b717de4f78 | ||
|
1216d218c1 | ||
|
209004ec6d | ||
|
c865d12849 | ||
|
c921c28185 | ||
|
3647943c80 | ||
|
4bf5381060 | ||
|
a10c358f83 | ||
|
d3c63b5699 | ||
|
c64334462f | ||
|
c497e19b99 | ||
|
2aeae616de | ||
|
e0e21530e2 | ||
|
2d7ff66246 | ||
|
179497e830 | ||
|
4c08c80e5b | ||
|
7b4b802a9b | ||
|
7d6af64904 | ||
|
16519f9486 | ||
|
bb27d38a12 | ||
|
5b26928751 | ||
|
f425e7e48f | ||
|
4601f46afb | ||
|
692bdc060c | ||
|
3a4f8c2e54 | ||
|
146c4284a2 | ||
|
5ae33b9f3b | ||
|
1f38b92ec6 | ||
|
f2a49a79f0 |
@@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
|||||||
|
|
||||||
PLAIN_API_KEY=
|
PLAIN_API_KEY=
|
||||||
PLAIN_WISH_LABEL_IDS=
|
PLAIN_WISH_LABEL_IDS=
|
||||||
|
|
||||||
|
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||||
|
4842
backend/package-lock.json
generated
4842
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",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
||||||
"dev:docker": "nodemon",
|
"dev:docker": "nodemon",
|
||||||
"build": "tsup",
|
"build": "tsup --sourcemap",
|
||||||
"build:frontend": "npm run build --prefix ../frontend",
|
"build:frontend": "npm run build --prefix ../frontend",
|
||||||
"start": "node dist/main.mjs",
|
"start": "node --enable-source-maps dist/main.mjs",
|
||||||
"type:check": "tsc --noEmit",
|
"type:check": "tsc --noEmit",
|
||||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
@@ -106,6 +106,7 @@
|
|||||||
"vitest": "^1.2.2"
|
"vitest": "^1.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||||
"@aws-sdk/client-iam": "^3.525.0",
|
"@aws-sdk/client-iam": "^3.525.0",
|
||||||
"@aws-sdk/client-kms": "^3.609.0",
|
"@aws-sdk/client-kms": "^3.609.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@peculiar/x509": "^1.10.0",
|
"@peculiar/x509": "^1.12.1",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "1.1.0",
|
"@sindresorhus/slugify": "1.1.0",
|
||||||
"@team-plain/typescript-sdk": "^4.6.1",
|
"@team-plain/typescript-sdk": "^4.6.1",
|
||||||
@@ -171,6 +172,7 @@
|
|||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
|
"pkijs": "^3.2.4",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.0.0",
|
||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
|||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-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 { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
@@ -160,6 +161,7 @@ declare module "fastify" {
|
|||||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
pkiCollection: TPkiCollectionServiceFactory;
|
pkiCollection: TPkiCollectionServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -53,6 +53,9 @@ import {
|
|||||||
TCertificateSecretsUpdate,
|
TCertificateSecretsUpdate,
|
||||||
TCertificatesInsert,
|
TCertificatesInsert,
|
||||||
TCertificatesUpdate,
|
TCertificatesUpdate,
|
||||||
|
TCertificateTemplateEstConfigs,
|
||||||
|
TCertificateTemplateEstConfigsInsert,
|
||||||
|
TCertificateTemplateEstConfigsUpdate,
|
||||||
TCertificateTemplates,
|
TCertificateTemplates,
|
||||||
TCertificateTemplatesInsert,
|
TCertificateTemplatesInsert,
|
||||||
TCertificateTemplatesUpdate,
|
TCertificateTemplatesUpdate,
|
||||||
@@ -372,6 +375,11 @@ declare module "knex/types/tables" {
|
|||||||
TCertificateTemplatesInsert,
|
TCertificateTemplatesInsert,
|
||||||
TCertificateTemplatesUpdate
|
TCertificateTemplatesUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
|
||||||
|
TCertificateTemplateEstConfigs,
|
||||||
|
TCertificateTemplateEstConfigsInsert,
|
||||||
|
TCertificateTemplateEstConfigsUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateBodies,
|
TCertificateBodies,
|
||||||
TCertificateBodiesInsert,
|
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({
|
export const AccessApprovalPoliciesApproversSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
approverId: z.string().uuid(),
|
|
||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
approverUserId: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||||
|
@@ -9,11 +9,12 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const AccessApprovalRequestsReviewersSchema = z.object({
|
export const AccessApprovalRequestsReviewersSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
member: z.string().uuid(),
|
member: z.string().uuid().nullable().optional(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
reviewerUserId: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;
|
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;
|
||||||
|
@@ -11,12 +11,13 @@ export const AccessApprovalRequestsSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
privilegeId: z.string().uuid().nullable().optional(),
|
privilegeId: z.string().uuid().nullable().optional(),
|
||||||
requestedBy: z.string().uuid(),
|
requestedBy: z.string().uuid().nullable().optional(),
|
||||||
isTemporary: z.boolean(),
|
isTemporary: z.boolean(),
|
||||||
temporaryRange: z.string().nullable().optional(),
|
temporaryRange: z.string().nullable().optional(),
|
||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
requestedByUserId: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||||
|
@@ -14,7 +14,8 @@ export const CertificateAuthorityCrlSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid(),
|
||||||
encryptedCrl: zodBuffer
|
encryptedCrl: zodBuffer,
|
||||||
|
caSecretId: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
|
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
|
||||||
|
29
backend/src/db/schemas/certificate-template-est-configs.ts
Normal file
29
backend/src/db/schemas/certificate-template-est-configs.ts
Normal file
@@ -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-authority-secret";
|
||||||
export * from "./certificate-bodies";
|
export * from "./certificate-bodies";
|
||||||
export * from "./certificate-secrets";
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificate-template-est-configs";
|
||||||
export * from "./certificate-templates";
|
export * from "./certificate-templates";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
CertificateAuthority = "certificate_authorities",
|
CertificateAuthority = "certificate_authorities",
|
||||||
|
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||||
CertificateAuthorityCert = "certificate_authority_certs",
|
CertificateAuthorityCert = "certificate_authority_certs",
|
||||||
CertificateAuthoritySecret = "certificate_authority_secret",
|
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||||
CertificateAuthorityCrl = "certificate_authority_crl",
|
CertificateAuthorityCrl = "certificate_authority_crl",
|
||||||
|
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const ProjectUserAdditionalPrivilegeSchema = z.object({
|
export const ProjectUserAdditionalPrivilegeSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
projectMembershipId: z.string().uuid(),
|
projectMembershipId: z.string().uuid().nullable().optional(),
|
||||||
isTemporary: z.boolean().default(false),
|
isTemporary: z.boolean().default(false),
|
||||||
temporaryMode: z.string().nullable().optional(),
|
temporaryMode: z.string().nullable().optional(),
|
||||||
temporaryRange: z.string().nullable().optional(),
|
temporaryRange: z.string().nullable().optional(),
|
||||||
@@ -18,7 +18,9 @@ export const ProjectUserAdditionalPrivilegeSchema = z.object({
|
|||||||
temporaryAccessEndTime: z.date().nullable().optional(),
|
temporaryAccessEndTime: z.date().nullable().optional(),
|
||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
projectId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;
|
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;
|
||||||
|
@@ -17,11 +17,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().trim().default("/"),
|
secretPath: z.string().trim().default("/"),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
approvers: z.string().array().min(1),
|
approverUserIds: z.string().array().min(1),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||||
path: ["approvals"],
|
path: ["approvals"],
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
}),
|
}),
|
||||||
@@ -56,7 +56,16 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
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,
|
actorOrgId: req.permission.orgId,
|
||||||
projectSlug: req.query.projectSlug
|
projectSlug: req.query.projectSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
return { approvals };
|
return { approvals };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -117,11 +127,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.trim()
|
.trim()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
.transform((val) => (val === "" ? "/" : val)),
|
||||||
approvers: z.string().array().min(1),
|
approverUserIds: z.string().array().min(1),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||||
path: ["approvals"],
|
path: ["approvals"],
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
}),
|
}),
|
||||||
|
@@ -1,10 +1,19 @@
|
|||||||
import { z } from "zod";
|
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 { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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) => {
|
export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
@@ -104,10 +113,11 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
}),
|
}),
|
||||||
reviewers: z
|
reviewers: z
|
||||||
.object({
|
.object({
|
||||||
member: z.string(),
|
userId: z.string(),
|
||||||
status: z.string()
|
status: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array(),
|
||||||
|
requestedByUser: approvalRequestUser
|
||||||
}).array()
|
}).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -1,86 +1,31 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { CA_CRLS } from "@app/lib/api-docs";
|
||||||
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
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) => {
|
export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:caId/crl",
|
url: "/:crlId",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
schema: {
|
schema: {
|
||||||
description: "Get CRL of the CA",
|
description: "Get CRL in DER format",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
|
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.instanceof(Buffer)
|
||||||
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req, res) => {
|
||||||
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
|
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
|
||||||
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({
|
res.header("Content-Type", "application/pkix-crl");
|
||||||
...req.auditLogInfo,
|
|
||||||
projectId: ca.projectId,
|
|
||||||
event: {
|
|
||||||
type: EventType.GET_CA_CRL,
|
|
||||||
metadata: {
|
|
||||||
caId: ca.id,
|
|
||||||
dn: ca.dn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return Buffer.from(crl);
|
||||||
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(
|
await server.register(
|
||||||
async (pkiRouter) => {
|
async (pkiRouter) => {
|
||||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
|
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||||
},
|
},
|
||||||
{ prefix: "/pki" }
|
{ prefix: "/pki" }
|
||||||
);
|
);
|
||||||
|
@@ -9,7 +9,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
|
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
|
||||||
try {
|
try {
|
||||||
const strBody = body instanceof Buffer ? body.toString() : body;
|
const strBody = body instanceof Buffer ? body.toString() : body;
|
||||||
|
if (!strBody) {
|
||||||
|
done(null, undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const json: unknown = JSON.parse(strBody);
|
const json: unknown = JSON.parse(strBody);
|
||||||
done(null, json);
|
done(null, json);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -474,18 +477,18 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
Operations: z.array(
|
Operations: z.array(
|
||||||
z.union([
|
z.union([
|
||||||
z.object({
|
z.object({
|
||||||
op: z.literal("replace"),
|
op: z.union([z.literal("replace"), z.literal("Replace")]),
|
||||||
value: z.object({
|
value: z.object({
|
||||||
id: z.string().trim(),
|
id: z.string().trim(),
|
||||||
displayName: z.string().trim()
|
displayName: z.string().trim()
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
op: z.literal("remove"),
|
op: z.union([z.literal("remove"), z.literal("Remove")]),
|
||||||
path: z.string().trim()
|
path: z.string().trim()
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
op: z.literal("add"),
|
op: z.union([z.literal("add"), z.literal("Add")]),
|
||||||
path: z.string().trim(),
|
path: z.string().trim(),
|
||||||
value: z.array(
|
value: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { 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>;
|
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||||
|
|
||||||
@@ -15,12 +15,12 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.AccessApprovalPolicyApprover,
|
TableName.AccessApprovalPolicyApprover,
|
||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${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("name").withSchema(TableName.Environment).as("envName"))
|
||||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
.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(), {
|
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
||||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
||||||
});
|
});
|
||||||
const formatedDoc = mergeOneToManyRelation(
|
const formattedDoc = sqlNestRelationships({
|
||||||
doc,
|
data: doc,
|
||||||
"id",
|
key: "id",
|
||||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
parentMapper: (data) => ({
|
||||||
...el,
|
environment: {
|
||||||
envId,
|
id: data.envId,
|
||||||
environment: { id: envId, name, slug }
|
name: data.envName,
|
||||||
|
slug: data.envSlug
|
||||||
|
},
|
||||||
|
projectId: data.projectId,
|
||||||
|
...AccessApprovalPoliciesSchema.parse(data)
|
||||||
}),
|
}),
|
||||||
({ approverId }) => approverId,
|
childrenMapper: [
|
||||||
"approvers"
|
{
|
||||||
);
|
key: "approverUserId",
|
||||||
return formatedDoc?.[0];
|
label: "userApprovers" as const,
|
||||||
|
mapper: ({ approverUserId }) => ({
|
||||||
|
userId: approverUserId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedDoc?.[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindById" });
|
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) => {
|
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
||||||
const formatedDoc = mergeOneToManyRelation(
|
|
||||||
docs,
|
const formattedDocs = sqlNestRelationships({
|
||||||
"id",
|
data: docs,
|
||||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
key: "id",
|
||||||
...el,
|
parentMapper: (data) => ({
|
||||||
envId,
|
environment: {
|
||||||
environment: { id: envId, name, slug }
|
id: data.envId,
|
||||||
|
name: data.envName,
|
||||||
|
slug: data.envSlug
|
||||||
|
},
|
||||||
|
projectId: data.projectId,
|
||||||
|
...AccessApprovalPoliciesSchema.parse(data)
|
||||||
|
// secretPath: data.secretPath || undefined,
|
||||||
}),
|
}),
|
||||||
({ approverId }) => approverId,
|
childrenMapper: [
|
||||||
"approvers"
|
{
|
||||||
);
|
key: "approverUserId",
|
||||||
return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined }));
|
label: "userApprovers" as const,
|
||||||
|
mapper: ({ approverUserId }) => ({
|
||||||
|
userId: approverUserId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedDocs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find" });
|
throw new DatabaseError({ error, name: "Find" });
|
||||||
}
|
}
|
||||||
|
@@ -34,8 +34,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL,
|
projectDAL
|
||||||
projectMembershipDAL
|
|
||||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||||
const createAccessApprovalPolicy = async ({
|
const createAccessApprovalPolicy = async ({
|
||||||
name,
|
name,
|
||||||
@@ -45,7 +44,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approverUserIds,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
environment,
|
environment,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
@@ -53,7 +52,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
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" });
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@@ -70,15 +69,6 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
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({
|
await verifyApprovers({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
@@ -86,7 +76,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
permissionService,
|
permissionService,
|
||||||
userIds: secretApprovers.map((approver) => approver.userId)
|
userIds: approverUserIds
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
@@ -101,8 +91,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
secretApprovers.map(({ id }) => ({
|
approverUserIds.map((userId) => ({
|
||||||
approverId: id,
|
approverUserId: userId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
@@ -138,7 +128,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
|
|
||||||
const updateAccessApprovalPolicy = async ({
|
const updateAccessApprovalPolicy = async ({
|
||||||
policyId,
|
policyId,
|
||||||
approvers,
|
approverUserIds,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -171,16 +161,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
if (approvers) {
|
if (approverUserIds) {
|
||||||
// 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 }
|
|
||||||
);
|
|
||||||
|
|
||||||
await verifyApprovers({
|
await verifyApprovers({
|
||||||
projectId: accessApprovalPolicy.projectId,
|
projectId: accessApprovalPolicy.projectId,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
@@ -188,15 +169,13 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
secretPath: doc.secretPath!,
|
secretPath: doc.secretPath!,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
permissionService,
|
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.delete({ policyId: doc.id }, tx);
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
secretApprovers.map(({ id }) => ({
|
approverUserIds.map((userId) => ({
|
||||||
approverId: id,
|
approverUserId: userId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
|
@@ -17,7 +17,7 @@ export type TCreateAccessApprovalPolicy = {
|
|||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: string[];
|
approverUserIds: string[];
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@@ -26,7 +26,7 @@ export type TCreateAccessApprovalPolicy = {
|
|||||||
export type TUpdateAccessApprovalPolicy = {
|
export type TUpdateAccessApprovalPolicy = {
|
||||||
policyId: string;
|
policyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
approvers?: string[];
|
approverUserIds?: string[];
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||||
|
|
||||||
@@ -40,6 +40,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.join<TUsers>(
|
||||||
|
db(TableName.Users).as("requestedByUser"),
|
||||||
|
`${TableName.AccessApprovalRequest}.requestedByUserId`,
|
||||||
|
`requestedByUser.id`
|
||||||
|
)
|
||||||
|
|
||||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
|
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
@@ -52,7 +58,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
||||||
)
|
)
|
||||||
|
|
||||||
.select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
|
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
|
||||||
.select(
|
.select(
|
||||||
db.ref("projectId").withSchema(TableName.Environment),
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
@@ -61,15 +67,20 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
.select(
|
.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")
|
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: ADD SUPPORT FOR GROUPS!!!!
|
||||||
.select(
|
.select(
|
||||||
db
|
db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
||||||
.ref("projectMembershipId")
|
db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
||||||
.as("privilegeMembershipId"),
|
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("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"),
|
||||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
|
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
|
||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
|
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
|
||||||
@@ -102,9 +113,18 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
enforcementLevel: doc.policyEnforcementLevel,
|
enforcementLevel: doc.policyEnforcementLevel,
|
||||||
envId: doc.policyEnvId
|
envId: doc.policyEnvId
|
||||||
},
|
},
|
||||||
|
requestedByUser: {
|
||||||
|
userId: doc.requestedByUserId,
|
||||||
|
email: doc.requestedByUserEmail,
|
||||||
|
firstName: doc.requestedByUserFirstName,
|
||||||
|
lastName: doc.requestedByUserLastName,
|
||||||
|
username: doc.requestedByUserUsername
|
||||||
|
},
|
||||||
privilege: doc.privilegeId
|
privilege: doc.privilegeId
|
||||||
? {
|
? {
|
||||||
membershipId: doc.privilegeMembershipId,
|
membershipId: doc.privilegeMembershipId,
|
||||||
|
userId: doc.privilegeUserId,
|
||||||
|
projectId: doc.projectId,
|
||||||
isTemporary: doc.privilegeIsTemporary,
|
isTemporary: doc.privilegeIsTemporary,
|
||||||
temporaryMode: doc.privilegeTemporaryMode,
|
temporaryMode: doc.privilegeTemporaryMode,
|
||||||
temporaryRange: doc.privilegeTemporaryRange,
|
temporaryRange: doc.privilegeTemporaryRange,
|
||||||
@@ -118,11 +138,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "reviewerMemberId",
|
key: "reviewerUserId",
|
||||||
label: "reviewers" as const,
|
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`
|
`${TableName.AccessApprovalPolicy}.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.join<TUsers>(
|
||||||
|
db(TableName.Users).as("requestedByUser"),
|
||||||
|
`${TableName.AccessApprovalRequest}.requestedByUserId`,
|
||||||
|
`requestedByUser.id`
|
||||||
|
)
|
||||||
|
|
||||||
.join(
|
.join(
|
||||||
TableName.AccessApprovalPolicyApprover,
|
TableName.AccessApprovalPolicyApprover,
|
||||||
`${TableName.AccessApprovalPolicy}.id`,
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.join<TUsers>(
|
||||||
|
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||||
|
"accessApprovalPolicyApproverUser.id"
|
||||||
|
)
|
||||||
|
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.AccessApprovalRequestReviewer,
|
TableName.AccessApprovalRequestReviewer,
|
||||||
`${TableName.AccessApprovalRequest}.id`,
|
`${TableName.AccessApprovalRequest}.id`,
|
||||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("accessApprovalReviewerUser"),
|
||||||
|
`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`,
|
||||||
|
`accessApprovalReviewerUser.id`
|
||||||
|
)
|
||||||
|
|
||||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
.select(
|
.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("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("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||||
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||||
tx.ref("projectId").withSchema(TableName.Environment),
|
tx.ref("projectId").withSchema(TableName.Environment),
|
||||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||||
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals")
|
||||||
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
@@ -189,15 +244,45 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPath: el.policySecretPath,
|
||||||
enforcementLevel: el.policyEnforcementLevel
|
enforcementLevel: el.policyEnforcementLevel
|
||||||
|
},
|
||||||
|
requestedByUser: {
|
||||||
|
userId: el.requestedByUserId,
|
||||||
|
email: el.requestedByUserEmail,
|
||||||
|
firstName: el.requestedByUserFirstName,
|
||||||
|
lastName: el.requestedByUserLastName,
|
||||||
|
username: el.requestedByUserUsername
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "reviewerMemberId",
|
key: "reviewerUserId",
|
||||||
label: "reviewers" as const,
|
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;
|
if (!formatedDoc?.[0]) return;
|
||||||
@@ -235,7 +320,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.where(`${TableName.Environment}.projectId`, projectId)
|
.where(`${TableName.Environment}.projectId`, projectId)
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
.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({
|
const formattedRequests = sqlNestRelationships({
|
||||||
data: accessRequests,
|
data: accessRequests,
|
||||||
@@ -245,9 +330,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "reviewerMemberId",
|
key: "reviewerUserId",
|
||||||
label: "reviewers" as const,
|
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">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
userDAL: Pick<TUserDALFactory, "findUserByProjectMembershipId" | "findUsersByProjectMembershipIds">;
|
userDAL: Pick<
|
||||||
|
TUserDALFactory,
|
||||||
|
"findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "find" | "findById"
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
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" });
|
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" });
|
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
|
||||||
|
|
||||||
await projectDAL.checkProjectUpgradeStatus(project.id);
|
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||||
@@ -114,13 +117,15 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
policyId: policy.id
|
policyId: policy.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const approverUsers = await userDAL.findUsersByProjectMembershipIds(
|
const approverUsers = await userDAL.find({
|
||||||
approvers.map((approver) => approver.approverId)
|
$in: {
|
||||||
);
|
id: approvers.map((approver) => approver.approverUserId)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const duplicateRequests = await accessApprovalRequestDAL.find({
|
const duplicateRequests = await accessApprovalRequestDAL.find({
|
||||||
policyId: policy.id,
|
policyId: policy.id,
|
||||||
requestedBy: membership.id,
|
requestedByUserId: actorId,
|
||||||
permissions: JSON.stringify(requestedPermissions),
|
permissions: JSON.stringify(requestedPermissions),
|
||||||
isTemporary
|
isTemporary
|
||||||
});
|
});
|
||||||
@@ -153,7 +158,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
const approvalRequest = await accessApprovalRequestDAL.create(
|
const approvalRequest = await accessApprovalRequestDAL.create(
|
||||||
{
|
{
|
||||||
policyId: policy.id,
|
policyId: policy.id,
|
||||||
requestedBy: membership.id,
|
requestedByUserId: actorId,
|
||||||
temporaryRange: temporaryRange || null,
|
temporaryRange: temporaryRange || null,
|
||||||
permissions: JSON.stringify(requestedPermissions),
|
permissions: JSON.stringify(requestedPermissions),
|
||||||
isTemporary
|
isTemporary
|
||||||
@@ -212,7 +217,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||||
|
|
||||||
if (authorProjectMembershipId) {
|
if (authorProjectMembershipId) {
|
||||||
requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId);
|
requests = requests.filter((request) => request.requestedByUserId === actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (envSlug) {
|
if (envSlug) {
|
||||||
@@ -246,8 +251,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user
|
accessApprovalRequest.requestedByUserId !== actorId && // 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
|
!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" });
|
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
|
||||||
}
|
}
|
||||||
@@ -273,7 +278,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
const review = await accessApprovalRequestReviewerDAL.findOne(
|
const review = await accessApprovalRequestReviewerDAL.findOne(
|
||||||
{
|
{
|
||||||
requestId: accessApprovalRequest.id,
|
requestId: accessApprovalRequest.id,
|
||||||
member: membership.id
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -282,7 +287,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
requestId: accessApprovalRequest.id,
|
requestId: accessApprovalRequest.id,
|
||||||
member: membership.id
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -303,7 +308,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
// Permanent access
|
// Permanent access
|
||||||
const privilege = await additionalPrivilegeDAL.create(
|
const privilege = await additionalPrivilegeDAL.create(
|
||||||
{
|
{
|
||||||
projectMembershipId: accessApprovalRequest.requestedBy,
|
userId: accessApprovalRequest.requestedByUserId,
|
||||||
|
projectId: accessApprovalRequest.projectId,
|
||||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||||
permissions: JSON.stringify(accessApprovalRequest.permissions)
|
permissions: JSON.stringify(accessApprovalRequest.permissions)
|
||||||
},
|
},
|
||||||
@@ -317,7 +323,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const privilege = await additionalPrivilegeDAL.create(
|
const privilege = await additionalPrivilegeDAL.create(
|
||||||
{
|
{
|
||||||
projectMembershipId: accessApprovalRequest.requestedBy,
|
userId: accessApprovalRequest.requestedByUserId,
|
||||||
|
projectId: accessApprovalRequest.projectId,
|
||||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||||
permissions: JSON.stringify(accessApprovalRequest.permissions),
|
permissions: JSON.stringify(accessApprovalRequest.permissions),
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
|
@@ -137,7 +137,7 @@ export enum EventType {
|
|||||||
GET_CA_CERT = "get-certificate-authority-cert",
|
GET_CA_CERT = "get-certificate-authority-cert",
|
||||||
SIGN_INTERMEDIATE = "sign-intermediate",
|
SIGN_INTERMEDIATE = "sign-intermediate",
|
||||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
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",
|
ISSUE_CERT = "issue-cert",
|
||||||
SIGN_CERT = "sign-cert",
|
SIGN_CERT = "sign-cert",
|
||||||
GET_CERT = "get-cert",
|
GET_CERT = "get-cert",
|
||||||
@@ -166,7 +166,10 @@ export enum EventType {
|
|||||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||||
DELETE_CERTIFICATE_TEMPLATE = "delete-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 {
|
interface UserActorMetadata {
|
||||||
@@ -1163,8 +1166,8 @@ interface ImportCaCert {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetCaCrl {
|
interface GetCaCrls {
|
||||||
type: EventType.GET_CA_CRL;
|
type: EventType.GET_CA_CRLS;
|
||||||
metadata: {
|
metadata: {
|
||||||
caId: string;
|
caId: string;
|
||||||
dn: string;
|
dn: string;
|
||||||
@@ -1420,6 +1423,29 @@ interface OrgAdminAccessProjectEvent {
|
|||||||
}; // no metadata yet
|
}; // 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 =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -1518,7 +1544,7 @@ export type Event =
|
|||||||
| GetCaCert
|
| GetCaCert
|
||||||
| SignIntermediate
|
| SignIntermediate
|
||||||
| ImportCaCert
|
| ImportCaCert
|
||||||
| GetCaCrl
|
| GetCaCrls
|
||||||
| IssueCert
|
| IssueCert
|
||||||
| SignCert
|
| SignCert
|
||||||
| GetCert
|
| GetCert
|
||||||
@@ -1547,4 +1573,7 @@ export type Event =
|
|||||||
| CreateCertificateTemplate
|
| CreateCertificateTemplate
|
||||||
| UpdateCertificateTemplate
|
| UpdateCertificateTemplate
|
||||||
| GetCertificateTemplate
|
| GetCertificateTemplate
|
||||||
| DeleteCertificateTemplate;
|
| DeleteCertificateTemplate
|
||||||
|
| CreateCertificateTemplateEstConfig
|
||||||
|
| UpdateCertificateTemplateEstConfig
|
||||||
|
| GetCertificateTemplateEstConfig;
|
||||||
|
@@ -2,24 +2,24 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
// import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
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 = {
|
type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
||||||
@@ -29,13 +29,42 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService,
|
permissionService // licenseService
|
||||||
licenseService
|
|
||||||
}: TCertificateAuthorityCrlServiceFactoryDep) => {
|
}: 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);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
@@ -52,15 +81,14 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateAuthorities
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
// const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan.caCrl)
|
// if (!plan.caCrl)
|
||||||
throw new BadRequestError({
|
// throw new BadRequestError({
|
||||||
message:
|
// message:
|
||||||
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
|
// "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 });
|
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
|
||||||
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
|
|
||||||
|
|
||||||
const keyId = await getProjectKmsCertificateKeyId({
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: ca.projectId,
|
projectId: ca.projectId,
|
||||||
@@ -72,15 +100,23 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
kmsId: keyId
|
kmsId: keyId
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
const decryptedCrls = await Promise.all(
|
||||||
const crl = new x509.X509Crl(decryptedCrl);
|
caCrls.map(async (caCrl) => {
|
||||||
|
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||||
|
const crl = new x509.X509Crl(decryptedCrl);
|
||||||
|
|
||||||
const base64crl = crl.toString("base64");
|
const base64crl = crl.toString("base64");
|
||||||
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
||||||
|
return {
|
||||||
|
id: caCrl.id,
|
||||||
|
crl: crlPem
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
crl: crlPem,
|
ca,
|
||||||
ca
|
crls: decryptedCrls
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,7 +202,8 @@ export const certificateAuthorityCrlServiceFactory = ({
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCaCrl
|
getCrlById,
|
||||||
|
getCaCrls
|
||||||
// rotateCaCrl
|
// rotateCaCrl
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetCrl = {
|
export type TGetCrlById = string;
|
||||||
|
|
||||||
|
export type TGetCaCrlsDTO = {
|
||||||
caId: string;
|
caId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||||
type: provider.type,
|
type: provider.type,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
@@ -0,0 +1,226 @@
|
|||||||
|
import {
|
||||||
|
CreateUserCommand,
|
||||||
|
CreateUserGroupCommand,
|
||||||
|
DeleteUserCommand,
|
||||||
|
DescribeReplicationGroupsCommand,
|
||||||
|
DescribeUserGroupsCommand,
|
||||||
|
ElastiCache,
|
||||||
|
ModifyReplicationGroupCommand,
|
||||||
|
ModifyUserGroupCommand
|
||||||
|
} from "@aws-sdk/client-elasticache";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const CreateElastiCacheUserSchema = z.object({
|
||||||
|
UserId: z.string().trim().min(1),
|
||||||
|
UserName: z.string().trim().min(1),
|
||||||
|
Engine: z.string().default("redis"),
|
||||||
|
Passwords: z.array(z.string().trim().min(1)).min(1).max(1), // Minimum password length is 16 characters, required by AWS.
|
||||||
|
AccessString: z.string().trim().min(1) // Example: "on ~* +@all"
|
||||||
|
});
|
||||||
|
|
||||||
|
const DeleteElasticCacheUserSchema = z.object({
|
||||||
|
UserId: z.string().trim().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
type TElastiCacheRedisUser = { userId: string; password: string };
|
||||||
|
type TBasicAWSCredentials = { accessKeyId: string; secretAccessKey: string };
|
||||||
|
|
||||||
|
type TCreateElastiCacheUserInput = z.infer<typeof CreateElastiCacheUserSchema>;
|
||||||
|
type TDeleteElastiCacheUserInput = z.infer<typeof DeleteElasticCacheUserSchema>;
|
||||||
|
|
||||||
|
const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => {
|
||||||
|
const elastiCache = new ElastiCache({
|
||||||
|
region,
|
||||||
|
credentials
|
||||||
|
});
|
||||||
|
const infisicalGroup = "infisical-managed-group-elasticache";
|
||||||
|
|
||||||
|
const ensureInfisicalGroupExists = async (clusterName: string) => {
|
||||||
|
const replicationGroups = await elastiCache.send(new DescribeUserGroupsCommand());
|
||||||
|
|
||||||
|
const existingGroup = replicationGroups.UserGroups?.find((group) => group.UserGroupId === infisicalGroup);
|
||||||
|
|
||||||
|
let newlyCreatedGroup = false;
|
||||||
|
if (!existingGroup) {
|
||||||
|
const createGroupCommand = new CreateUserGroupCommand({
|
||||||
|
UserGroupId: infisicalGroup,
|
||||||
|
UserIds: ["default"],
|
||||||
|
Engine: "redis"
|
||||||
|
});
|
||||||
|
|
||||||
|
await elastiCache.send(createGroupCommand);
|
||||||
|
newlyCreatedGroup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingGroup || newlyCreatedGroup) {
|
||||||
|
const replicationGroup = (
|
||||||
|
await elastiCache.send(
|
||||||
|
new DescribeReplicationGroupsCommand({
|
||||||
|
ReplicationGroupId: clusterName
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).ReplicationGroups?.[0];
|
||||||
|
|
||||||
|
if (!replicationGroup?.UserGroupIds?.includes(infisicalGroup)) {
|
||||||
|
// If the replication group doesn't have the infisical user group, we need to associate it
|
||||||
|
const modifyGroupCommand = new ModifyReplicationGroupCommand({
|
||||||
|
UserGroupIdsToAdd: [infisicalGroup],
|
||||||
|
UserGroupIdsToRemove: [],
|
||||||
|
ApplyImmediately: true,
|
||||||
|
ReplicationGroupId: clusterName
|
||||||
|
});
|
||||||
|
await elastiCache.send(modifyGroupCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUserToInfisicalGroup = async (userId: string) => {
|
||||||
|
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
|
||||||
|
|
||||||
|
const addUserToGroupCommand = new ModifyUserGroupCommand({
|
||||||
|
UserGroupId: infisicalGroup,
|
||||||
|
UserIdsToAdd: [userId],
|
||||||
|
UserIdsToRemove: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await elastiCache.send(addUserToGroupCommand);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = async (creationInput: TCreateElastiCacheUserInput, clusterName: string) => {
|
||||||
|
await ensureInfisicalGroupExists(clusterName);
|
||||||
|
|
||||||
|
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
|
||||||
|
await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: creationInput.UserId,
|
||||||
|
password: creationInput.Passwords[0]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (
|
||||||
|
deletionInput: TDeleteElastiCacheUserInput
|
||||||
|
): Promise<Pick<TElastiCacheRedisUser, "userId">> => {
|
||||||
|
await elastiCache.send(new DeleteUserCommand(deletionInput));
|
||||||
|
return { userId: deletionInput.UserId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyCredentials = async (clusterName: string) => {
|
||||||
|
await elastiCache.send(
|
||||||
|
new DescribeReplicationGroupsCommand({
|
||||||
|
ReplicationGroupId: clusterName
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createUser,
|
||||||
|
deleteUser,
|
||||||
|
verifyCredentials
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
|
||||||
|
return `inf-${customAlphabet(charset, 32)()}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = DynamicSecretAwsElastiCacheSchema.parse(inputs);
|
||||||
|
|
||||||
|
// We need to ensure the that the creation & revocation statements are valid and can be used to create and revoke users.
|
||||||
|
// We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements.
|
||||||
|
CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement));
|
||||||
|
DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement));
|
||||||
|
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
await ElastiCacheUserManager(
|
||||||
|
{
|
||||||
|
accessKeyId: providerInputs.accessKeyId,
|
||||||
|
secretAccessKey: providerInputs.secretAccessKey
|
||||||
|
},
|
||||||
|
providerInputs.region
|
||||||
|
).verifyCredentials(providerInputs.clusterName);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
if (!(await validateConnection(providerInputs))) {
|
||||||
|
throw new BadRequestError({ message: "Failed to establish connection" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaseUsername = generateUsername();
|
||||||
|
const leasePassword = generatePassword();
|
||||||
|
const leaseExpiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
|
username: leaseUsername,
|
||||||
|
password: leasePassword,
|
||||||
|
expiration: leaseExpiration
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement));
|
||||||
|
|
||||||
|
await ElastiCacheUserManager(
|
||||||
|
{
|
||||||
|
accessKeyId: providerInputs.accessKeyId,
|
||||||
|
secretAccessKey: providerInputs.secretAccessKey
|
||||||
|
},
|
||||||
|
providerInputs.region
|
||||||
|
).createUser(parsedStatement, providerInputs.clusterName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entityId: leaseUsername,
|
||||||
|
data: {
|
||||||
|
DB_USERNAME: leaseUsername,
|
||||||
|
DB_PASSWORD: leasePassword
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId });
|
||||||
|
const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement));
|
||||||
|
|
||||||
|
await ElastiCacheUserManager(
|
||||||
|
{
|
||||||
|
accessKeyId: providerInputs.accessKeyId,
|
||||||
|
secretAccessKey: providerInputs.secretAccessKey
|
||||||
|
},
|
||||||
|
providerInputs.region
|
||||||
|
).deleteUser(parsedStatement);
|
||||||
|
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Do nothing
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@@ -1,10 +1,14 @@
|
|||||||
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
|
import { RedisDatabaseProvider } from "./redis";
|
||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = () => ({
|
export const buildDynamicSecretProviders = () => ({
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider()
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||||
|
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||||
|
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider()
|
||||||
});
|
});
|
||||||
|
@@ -7,6 +7,29 @@ export enum SqlProviders {
|
|||||||
MsSQL = "mssql"
|
MsSQL = "mssql"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DynamicSecretRedisDBSchema = z.object({
|
||||||
|
host: z.string().trim().toLowerCase(),
|
||||||
|
port: z.number(),
|
||||||
|
username: z.string().trim(), // this is often "default".
|
||||||
|
password: z.string().trim().optional(),
|
||||||
|
|
||||||
|
creationStatement: z.string().trim(),
|
||||||
|
revocationStatement: z.string().trim(),
|
||||||
|
renewStatement: z.string().trim().optional(),
|
||||||
|
ca: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DynamicSecretAwsElastiCacheSchema = z.object({
|
||||||
|
clusterName: z.string().trim().min(1),
|
||||||
|
accessKeyId: z.string().trim().min(1),
|
||||||
|
secretAccessKey: z.string().trim().min(1),
|
||||||
|
|
||||||
|
region: z.string().trim(),
|
||||||
|
creationStatement: z.string().trim(),
|
||||||
|
revocationStatement: z.string().trim(),
|
||||||
|
ca: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export const DynamicSecretSqlDBSchema = z.object({
|
export const DynamicSecretSqlDBSchema = z.object({
|
||||||
client: z.nativeEnum(SqlProviders),
|
client: z.nativeEnum(SqlProviders),
|
||||||
host: z.string().trim().toLowerCase(),
|
host: z.string().trim().toLowerCase(),
|
||||||
@@ -47,13 +70,17 @@ export const DynamicSecretAwsIamSchema = z.object({
|
|||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra",
|
Cassandra = "cassandra",
|
||||||
AwsIam = "aws-iam"
|
AwsIam = "aws-iam",
|
||||||
|
Redis = "redis",
|
||||||
|
AwsElastiCache = "aws-elasticache"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
183
backend/src/ee/services/dynamic-secret/providers/redis.ts
Normal file
183
backend/src/ee/services/dynamic-secret/providers/redis.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
import { Redis } from "ioredis";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { getDbConnectionHost } from "@app/lib/knex";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(32);
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeTransactions = async (connection: Redis, commands: string[]): Promise<(string | null)[] | null> => {
|
||||||
|
// Initiate a transaction
|
||||||
|
const pipeline = connection.multi();
|
||||||
|
|
||||||
|
// Add all commands to the pipeline
|
||||||
|
for (const command of commands) {
|
||||||
|
const args = command
|
||||||
|
.split(" ")
|
||||||
|
.map((arg) => arg.trim())
|
||||||
|
.filter((arg) => arg.length > 0);
|
||||||
|
pipeline.call(args[0], ...args.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the transaction
|
||||||
|
const results = await pipeline.exec();
|
||||||
|
|
||||||
|
if (!results) {
|
||||||
|
throw new BadRequestError({ message: "Redis transaction failed: No results returned" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for errors in the results
|
||||||
|
const errors = results.filter(([err]) => err !== null);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new BadRequestError({ message: "Redis transaction failed with errors" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
return results.map(([_, result]) => result as string | null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||||
|
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||||
|
|
||||||
|
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
|
||||||
|
if (
|
||||||
|
isCloud &&
|
||||||
|
// localhost
|
||||||
|
// internal ips
|
||||||
|
(providerInputs.host === "host.docker.internal" ||
|
||||||
|
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||||
|
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||||
|
)
|
||||||
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
|
||||||
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||||
|
let connection: Redis | null = null;
|
||||||
|
try {
|
||||||
|
connection = new Redis({
|
||||||
|
username: providerInputs.username,
|
||||||
|
host: providerInputs.host,
|
||||||
|
port: providerInputs.port,
|
||||||
|
password: providerInputs.password,
|
||||||
|
...(providerInputs.ca && {
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
ca: providerInputs.ca
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let result: string;
|
||||||
|
if (providerInputs.password) {
|
||||||
|
result = await connection.auth(providerInputs.username, providerInputs.password, () => {});
|
||||||
|
} else {
|
||||||
|
result = await connection.auth(providerInputs.username, () => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result !== "OK") {
|
||||||
|
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
} catch (err) {
|
||||||
|
if (connection) await connection.quit();
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const pingResponse = await connection
|
||||||
|
.ping()
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
return pingResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
expiration
|
||||||
|
});
|
||||||
|
|
||||||
|
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||||
|
|
||||||
|
await executeTransactions(connection, queries);
|
||||||
|
|
||||||
|
await connection.quit();
|
||||||
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = entityId;
|
||||||
|
|
||||||
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
|
|
||||||
|
await executeTransactions(connection, queries);
|
||||||
|
|
||||||
|
await connection.quit();
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = entityId;
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
||||||
|
|
||||||
|
if (renewStatement) {
|
||||||
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
|
await executeTransactions(connection, queries);
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.quit();
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@@ -126,7 +126,6 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
@@ -66,6 +66,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.GroupProjectMembership}.id`
|
`${TableName.GroupProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
@@ -73,6 +74,12 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectUserAdditionalPrivilege,
|
||||||
|
`${TableName.GroupProjectMembership}.projectId`,
|
||||||
|
`${TableName.Project}.id`
|
||||||
|
)
|
||||||
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
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("projectId").withSchema(TableName.GroupProjectMembership),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
)
|
|
||||||
.select("permissions");
|
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)
|
const docs = await db(TableName.ProjectMembership)
|
||||||
.join(
|
.join(
|
||||||
@@ -98,12 +126,13 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectUserAdditionalPrivilege,
|
TableName.ProjectUserAdditionalPrivilege,
|
||||||
`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`,
|
`${TableName.ProjectUserAdditionalPrivilege}.projectId`,
|
||||||
`${TableName.ProjectMembership}.id`
|
`${TableName.ProjectMembership}.projectId`
|
||||||
)
|
)
|
||||||
|
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
.where("userId", userId)
|
.where(`${TableName.ProjectMembership}.userId`, userId)
|
||||||
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
||||||
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
@@ -120,6 +149,10 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
||||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
||||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
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
|
db
|
||||||
.ref("temporaryAccessStartTime")
|
.ref("temporaryAccessStartTime")
|
||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
@@ -198,6 +231,31 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
customRoleSlug: z.string().optional().nullable()
|
customRoleSlug: z.string().optional().nullable()
|
||||||
}).parse(data)
|
}).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)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
const activeAdditionalPrivileges =
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
permission?.[0]?.additionalPrivileges?.filter(
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
({ 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 {
|
return {
|
||||||
...(permission[0] || groupPermission[0]),
|
...(permission[0] || groupPermission[0]),
|
||||||
roles: [...activeRoles, ...activeGroupRoles],
|
roles: [...activeRoles, ...activeGroupRoles],
|
||||||
additionalPrivileges: activeAdditionalPrivileges
|
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
|
@@ -18,7 +18,7 @@ import {
|
|||||||
|
|
||||||
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
|
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
|
||||||
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
|
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,12 +53,17 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
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 (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||||
|
|
||||||
if (!dto.isTemporary) {
|
if (!dto.isTemporary) {
|
||||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
||||||
projectMembershipId,
|
userId: projectMembership.userId,
|
||||||
|
projectId: projectMembership.projectId,
|
||||||
slug,
|
slug,
|
||||||
permissions: customPermission
|
permissions: customPermission
|
||||||
});
|
});
|
||||||
@@ -67,7 +72,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
|
|
||||||
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
||||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
||||||
projectMembershipId,
|
projectId: projectMembership.projectId,
|
||||||
|
userId: projectMembership.userId,
|
||||||
slug,
|
slug,
|
||||||
permissions: customPermission,
|
permissions: customPermission,
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
@@ -90,7 +96,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
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" });
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@@ -105,7 +115,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
if (dto?.slug) {
|
if (dto?.slug) {
|
||||||
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
slug: dto.slug,
|
slug: dto.slug,
|
||||||
projectMembershipId: projectMembership.id
|
userId: projectMembership.id,
|
||||||
|
projectId: projectMembership.projectId
|
||||||
});
|
});
|
||||||
if (existingSlug && existingSlug.id !== userPrivilege.id)
|
if (existingSlug && existingSlug.id !== userPrivilege.id)
|
||||||
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||||
@@ -138,7 +149,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
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" });
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@@ -164,7 +178,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
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" });
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@@ -198,7 +215,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
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;
|
return userPrivileges;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -50,8 +50,8 @@ export const buildScimUser = ({
|
|||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
username: string;
|
username: string;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
firstName: string;
|
firstName: string | null | undefined;
|
||||||
lastName: string;
|
lastName: string | null | undefined;
|
||||||
groups?: {
|
groups?: {
|
||||||
value: string;
|
value: string;
|
||||||
display: string;
|
display: string;
|
||||||
@@ -64,9 +64,9 @@ export const buildScimUser = ({
|
|||||||
userName: username,
|
userName: username,
|
||||||
displayName: `${firstName} ${lastName}`,
|
displayName: `${firstName} ${lastName}`,
|
||||||
name: {
|
name: {
|
||||||
givenName: firstName,
|
givenName: firstName || "",
|
||||||
middleName: null,
|
middleName: null,
|
||||||
familyName: lastName
|
familyName: lastName || ""
|
||||||
},
|
},
|
||||||
emails: email
|
emails: email
|
||||||
? [
|
? [
|
||||||
|
@@ -31,6 +31,7 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
|
|||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import {
|
import {
|
||||||
buildScimGroup,
|
buildScimGroup,
|
||||||
buildScimGroupList,
|
buildScimGroupList,
|
||||||
@@ -93,6 +94,7 @@ type TScimServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||||
@@ -112,6 +114,7 @@ export const scimServiceFactory = ({
|
|||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
smtpService
|
smtpService
|
||||||
}: TScimServiceFactoryDep) => {
|
}: TScimServiceFactoryDep) => {
|
||||||
const createScimToken = async ({
|
const createScimToken = async ({
|
||||||
@@ -264,8 +267,8 @@ export const scimServiceFactory = ({
|
|||||||
orgMembershipId: membership.id,
|
orgMembershipId: membership.id,
|
||||||
username: membership.externalId ?? membership.username,
|
username: membership.externalId ?? membership.username,
|
||||||
email: membership.email ?? "",
|
email: membership.email ?? "",
|
||||||
firstName: membership.firstName as string,
|
firstName: membership.firstName,
|
||||||
lastName: membership.lastName as string,
|
lastName: membership.lastName,
|
||||||
active: membership.isActive,
|
active: membership.isActive,
|
||||||
groups: groupMembershipsInOrg.map((group) => ({
|
groups: groupMembershipsInOrg.map((group) => ({
|
||||||
value: group.groupId,
|
value: group.groupId,
|
||||||
@@ -424,8 +427,8 @@ export const scimServiceFactory = ({
|
|||||||
return buildScimUser({
|
return buildScimUser({
|
||||||
orgMembershipId: createdOrgMembership.id,
|
orgMembershipId: createdOrgMembership.id,
|
||||||
username: externalId,
|
username: externalId,
|
||||||
firstName: createdUser.firstName as string,
|
firstName: createdUser.firstName,
|
||||||
lastName: createdUser.lastName as string,
|
lastName: createdUser.lastName,
|
||||||
email: createdUser.email ?? "",
|
email: createdUser.email ?? "",
|
||||||
active: createdOrgMembership.isActive
|
active: createdOrgMembership.isActive
|
||||||
});
|
});
|
||||||
@@ -480,8 +483,8 @@ export const scimServiceFactory = ({
|
|||||||
orgMembershipId: membership.id,
|
orgMembershipId: membership.id,
|
||||||
username: membership.externalId ?? membership.username,
|
username: membership.externalId ?? membership.username,
|
||||||
email: membership.email,
|
email: membership.email,
|
||||||
firstName: membership.firstName as string,
|
firstName: membership.firstName,
|
||||||
lastName: membership.lastName as string,
|
lastName: membership.lastName,
|
||||||
active
|
active
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -524,8 +527,8 @@ export const scimServiceFactory = ({
|
|||||||
orgMembershipId: membership.id,
|
orgMembershipId: membership.id,
|
||||||
username: membership.externalId ?? membership.username,
|
username: membership.externalId ?? membership.username,
|
||||||
email: membership.email,
|
email: membership.email,
|
||||||
firstName: membership.firstName as string,
|
firstName: membership.firstName,
|
||||||
lastName: membership.lastName as string,
|
lastName: membership.lastName,
|
||||||
active,
|
active,
|
||||||
groups: groupMembershipsInOrg.map((group) => ({
|
groups: groupMembershipsInOrg.map((group) => ({
|
||||||
value: group.groupId,
|
value: group.groupId,
|
||||||
@@ -558,6 +561,7 @@ export const scimServiceFactory = ({
|
|||||||
orgId: membership.orgId,
|
orgId: membership.orgId,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
licenseService
|
licenseService
|
||||||
@@ -880,59 +884,50 @@ export const scimServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for await (const operation of operations) {
|
for await (const operation of operations) {
|
||||||
switch (operation.op) {
|
if (operation.op === "replace" || operation.op === "Replace") {
|
||||||
case "replace": {
|
group = await groupDAL.updateById(group.id, {
|
||||||
group = await groupDAL.updateById(group.id, {
|
name: operation.value.displayName
|
||||||
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({
|
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({
|
|
||||||
group,
|
group,
|
||||||
userIds: [orgMembership.userId as string],
|
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||||
userDAL,
|
userDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
orgDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
projectKeyDAL
|
projectKeyDAL,
|
||||||
});
|
projectDAL,
|
||||||
break;
|
projectBotDAL
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new ScimRequestError({
|
|
||||||
detail: "Invalid Operation",
|
|
||||||
status: 400
|
|
||||||
});
|
});
|
||||||
|
} 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)[];
|
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 = {
|
type TReplaceOp = {
|
||||||
op: "replace";
|
op: "replace" | "Replace";
|
||||||
value: {
|
value: {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -119,12 +121,12 @@ type TReplaceOp = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type TRemoveOp = {
|
type TRemoveOp = {
|
||||||
op: "remove";
|
op: "remove" | "Remove";
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TAddOp = {
|
type TAddOp = {
|
||||||
op: "add";
|
op: "add" | "Add";
|
||||||
path: string;
|
path: string;
|
||||||
value: {
|
value: {
|
||||||
value: string;
|
value: string;
|
||||||
|
@@ -20,7 +20,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${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(
|
.select(
|
||||||
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
@@ -47,8 +55,11 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
{
|
{
|
||||||
key: "approverUserId",
|
key: "approverUserId",
|
||||||
label: "userApprovers" as const,
|
label: "userApprovers" as const,
|
||||||
mapper: ({ approverUserId }) => ({
|
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
|
||||||
userId: approverUserId
|
userId: approverUserId,
|
||||||
|
email: approverEmail,
|
||||||
|
firstName: approverFirstName,
|
||||||
|
lastName: approverLastName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
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.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 { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
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 { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
|
||||||
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
|
||||||
|
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
|
||||||
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
|
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
|
||||||
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
|
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
|
||||||
import {
|
import {
|
||||||
@@ -89,7 +91,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
|
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
|
projectDAL: Pick<
|
||||||
|
TProjectDALFactory,
|
||||||
|
"checkProjectUpgradeStatus" | "findById" | "findProjectById" | "findProjectWithOrg"
|
||||||
|
>;
|
||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||||
secretV2BridgeDAL: Pick<
|
secretV2BridgeDAL: Pick<
|
||||||
@@ -98,6 +103,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,6 +127,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
@@ -1061,6 +1068,15 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
}
|
}
|
||||||
return { ...doc, commits: approvalCommits };
|
return { ...doc, commits: approvalCommits };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await sendApprovalEmailsFn({
|
||||||
|
projectDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
secretApprovalRequest,
|
||||||
|
smtpService,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
return secretApprovalRequest;
|
return secretApprovalRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1311,8 +1327,17 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...doc, commits: approvalCommits };
|
return { ...doc, commits: approvalCommits };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await sendApprovalEmailsFn({
|
||||||
|
projectDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
secretApprovalRequest,
|
||||||
|
smtpService,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
return secretApprovalRequest;
|
return secretApprovalRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,11 +19,15 @@ export const KeyStorePrefixes = {
|
|||||||
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
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 = {
|
export const KeyStoreTtls = {
|
||||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10
|
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10,
|
||||||
|
AccessTokenStatusUpdateInSeconds: 120
|
||||||
};
|
};
|
||||||
|
|
||||||
type TWaitTillReady = {
|
type TWaitTillReady = {
|
||||||
|
@@ -1120,9 +1120,10 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
certificateChain: "The certificate chain of the issued certificate",
|
certificateChain: "The certificate chain of the issued certificate",
|
||||||
serialNumber: "The serial number of the issued certificate"
|
serialNumber: "The serial number of the issued certificate"
|
||||||
},
|
},
|
||||||
GET_CRL: {
|
GET_CRLS: {
|
||||||
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
caId: "The ID of the CA to get the certificate revocation lists (CRLs) for",
|
||||||
crl: "The certificate revocation list (CRL) of the CA"
|
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 = {
|
export const ALERTS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the alert in",
|
projectId: "The ID of the project to create the alert in",
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Logger } from "pino";
|
import { Logger } from "pino";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { removeTrailingSlash } from "../fn";
|
||||||
import { zpStr } from "../zod";
|
import { zpStr } from "../zod";
|
||||||
|
|
||||||
export const GITLAB_URL = "https://gitlab.com";
|
export const GITLAB_URL = "https://gitlab.com";
|
||||||
@@ -63,7 +64,9 @@ const envSchema = z
|
|||||||
.string()
|
.string()
|
||||||
.min(32)
|
.min(32)
|
||||||
.default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"),
|
.default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"),
|
||||||
SITE_URL: zpStr(z.string().optional()),
|
|
||||||
|
// Ensure that the SITE_URL never ends with a trailing slash
|
||||||
|
SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional(),
|
||||||
// Telemetry
|
// Telemetry
|
||||||
TELEMETRY_ENABLED: zodStrBool.default("true"),
|
TELEMETRY_ENABLED: zodStrBool.default("true"),
|
||||||
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
|
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
|
||||||
@@ -74,6 +77,7 @@ const envSchema = z
|
|||||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||||
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||||
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
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_MFA_LIFETIME: zpStr(z.string().default("5m")),
|
||||||
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
|
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
|
||||||
// Oauth
|
// Oauth
|
||||||
@@ -141,7 +145,8 @@ const envSchema = z
|
|||||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||||
PLAIN_API_KEY: zpStr(z.string().optional()),
|
PLAIN_API_KEY: zpStr(z.string().optional()),
|
||||||
PLAIN_WISH_LABEL_IDS: 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) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
|
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export const secondsToMillis = (seconds: number) => seconds * 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",
|
CaCrlRotation = "ca-crl-rotation",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and 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 {
|
export enum QueueJobs {
|
||||||
@@ -48,7 +49,9 @@ export enum QueueJobs {
|
|||||||
CaCrlRotation = "ca-crl-rotation-job",
|
CaCrlRotation = "ca-crl-rotation-job",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and 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 = {
|
export type TQueueJobTypes = {
|
||||||
@@ -148,6 +151,15 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.ProjectV3Migration;
|
name: QueueJobs.ProjectV3Migration;
|
||||||
payload: { projectId: string };
|
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>;
|
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;
|
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
|
||||||
}
|
}
|
||||||
const authHeader = req.headers?.authorization;
|
const authHeader = req.headers?.authorization;
|
||||||
|
|
||||||
if (!authHeader) return { authMode: null, token: null };
|
if (!authHeader) return { authMode: null, token: null };
|
||||||
|
|
||||||
const authTokenValue = authHeader.slice(7); // slice of after Bearer
|
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.decorateRequest("auth", null);
|
||||||
server.addHook("onRequest", async (req) => {
|
server.addHook("onRequest", async (req) => {
|
||||||
const appCfg = getConfig();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||||
|
|
||||||
if (!authMode) return;
|
if (!authMode) return;
|
||||||
|
|
||||||
switch (authMode) {
|
switch (authMode) {
|
||||||
|
173
backend/src/server/routes/est/certificate-est-router.ts
Normal file
173
backend/src/server/routes/est/certificate-est-router.ts
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,4 +1,5 @@
|
|||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
|
import { Redis } from "ioredis";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -71,8 +72,10 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
|
|||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
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 { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||||
@@ -89,7 +92,9 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
|||||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
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 { 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 { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
@@ -194,6 +199,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
|
|||||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||||
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
||||||
|
import { registerCertificateEstRouter } from "./est/certificate-est-router";
|
||||||
import { registerV1Routes } from "./v1";
|
import { registerV1Routes } from "./v1";
|
||||||
import { registerV2Routes } from "./v2";
|
import { registerV2Routes } from "./v2";
|
||||||
import { registerV3Routes } from "./v3";
|
import { registerV3Routes } from "./v3";
|
||||||
@@ -412,6 +418,7 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
@@ -475,8 +482,12 @@ export const registerRoutes = async (
|
|||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
projectBotDAL,
|
||||||
incidentContactDAL,
|
incidentContactDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
@@ -496,6 +507,8 @@ export const registerRoutes = async (
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
orgService,
|
orgService,
|
||||||
licenseService
|
licenseService
|
||||||
@@ -549,10 +562,12 @@ export const registerRoutes = async (
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
|
groupProjectDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
|
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
|
||||||
@@ -590,6 +605,7 @@ export const registerRoutes = async (
|
|||||||
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||||
const certificateTemplateDAL = certificateTemplateDALFactory(db);
|
const certificateTemplateDAL = certificateTemplateDALFactory(db);
|
||||||
|
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
|
||||||
|
|
||||||
const certificateDAL = certificateDALFactory(db);
|
const certificateDAL = certificateDALFactory(db);
|
||||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
@@ -641,14 +657,27 @@ export const registerRoutes = async (
|
|||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService,
|
permissionService
|
||||||
licenseService
|
// licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateTemplateService = certificateTemplateServiceFactory({
|
const certificateTemplateService = certificateTemplateServiceFactory({
|
||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
|
certificateTemplateEstConfigDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateEstService = certificateEstServiceFactory({
|
||||||
|
certificateAuthorityService,
|
||||||
|
certificateTemplateService,
|
||||||
|
certificateTemplateDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const pkiAlertService = pkiAlertServiceFactory({
|
const pkiAlertService = pkiAlertServiceFactory({
|
||||||
@@ -678,6 +707,7 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
orgService,
|
orgService,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
projectRoleDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
@@ -834,6 +864,7 @@ export const registerRoutes = async (
|
|||||||
secretQueueService,
|
secretQueueService,
|
||||||
kmsService,
|
kmsService,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
@@ -949,12 +980,20 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const accessTokenQueue = accessTokenQueueServiceFactory({
|
||||||
|
keyStore,
|
||||||
|
identityAccessTokenDAL,
|
||||||
|
queueService,
|
||||||
|
serviceTokenDAL
|
||||||
|
});
|
||||||
|
|
||||||
const serviceTokenService = serviceTokenServiceFactory({
|
const serviceTokenService = serviceTokenServiceFactory({
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
serviceTokenDAL,
|
serviceTokenDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
accessTokenQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityService = identityServiceFactory({
|
const identityService = identityServiceFactory({
|
||||||
@@ -964,10 +1003,13 @@ export const registerRoutes = async (
|
|||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityOrgMembershipDAL
|
identityOrgMembershipDAL,
|
||||||
|
accessTokenQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityProjectService = identityProjectServiceFactory({
|
const identityProjectService = identityProjectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -1173,6 +1215,7 @@ export const registerRoutes = async (
|
|||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
|
certificateEst: certificateEstService,
|
||||||
pkiAlert: pkiAlertService,
|
pkiAlert: pkiAlertService,
|
||||||
pkiCollection: pkiCollectionService,
|
pkiCollection: pkiCollectionService,
|
||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
@@ -1216,7 +1259,7 @@ export const registerRoutes = async (
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
date: z.date(),
|
date: z.date(),
|
||||||
message: z.literal("Ok"),
|
message: z.string().optional(),
|
||||||
emailConfigured: z.boolean().optional(),
|
emailConfigured: z.boolean().optional(),
|
||||||
inviteOnlySignup: z.boolean().optional(),
|
inviteOnlySignup: z.boolean().optional(),
|
||||||
redisConfigured: z.boolean().optional(),
|
redisConfigured: z.boolean().optional(),
|
||||||
@@ -1225,12 +1268,37 @@ export const registerRoutes = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async (request, reply) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.raw("SELECT NOW()");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Health check: database connection failed", err);
|
||||||
|
return reply.code(503).send({
|
||||||
|
date: new Date(),
|
||||||
|
message: "Service unavailable"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.isRedisConfigured) {
|
||||||
|
const redis = new Redis(cfg.REDIS_URL);
|
||||||
|
try {
|
||||||
|
await redis.ping();
|
||||||
|
redis.disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Health check: redis connection failed", err);
|
||||||
|
return reply.code(503).send({
|
||||||
|
date: new Date(),
|
||||||
|
message: "Service unavailable"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
message: "Ok" as const,
|
message: "Ok",
|
||||||
emailConfigured: cfg.isSmtpConfigured,
|
emailConfigured: cfg.isSmtpConfigured,
|
||||||
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
||||||
redisConfigured: cfg.isRedisConfigured,
|
redisConfigured: cfg.isRedisConfigured,
|
||||||
@@ -1240,6 +1308,9 @@ export const registerRoutes = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// register special routes
|
||||||
|
await server.register(registerCertificateEstRouter, { prefix: "/.well-known/est" });
|
||||||
|
|
||||||
// register routes for v1
|
// register routes for v1
|
||||||
await server.register(
|
await server.register(
|
||||||
async (v1Server) => {
|
async (v1Server) => {
|
||||||
|
@@ -669,6 +669,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||||
await server.services.certificateAuthority.signCertFromCa({
|
await server.services.certificateAuthority.signCertFromCa({
|
||||||
|
isInternal: false,
|
||||||
caId: req.params.caId,
|
caId: req.params.caId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -691,11 +692,90 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate,
|
certificate: certificate.toString("pem"),
|
||||||
certificateChain,
|
certificateChain,
|
||||||
issuingCaCertificate,
|
issuingCaCertificate,
|
||||||
serialNumber
|
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) => {
|
handler: async (req) => {
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||||
await server.services.certificateAuthority.signCertFromCa({
|
await server.services.certificateAuthority.signCertFromCa({
|
||||||
|
isInternal: false,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@@ -231,7 +232,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate,
|
certificate: certificate.toString("pem"),
|
||||||
certificateChain,
|
certificateChain,
|
||||||
issuingCaCertificate,
|
issuingCaCertificate,
|
||||||
serialNumber
|
serialNumber
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
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 { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
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) => {
|
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -202,4 +209,141 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
return certificateTemplate;
|
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({
|
querystring: z.object({
|
||||||
teamId: z.string().trim().optional(),
|
teamId: z.string().trim().optional(),
|
||||||
|
azureDevOpsOrgName: z.string().trim().optional(),
|
||||||
workspaceSlug: z.string().trim().optional()
|
workspaceSlug: z.string().trim().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
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 { inviteUserRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -16,23 +16,37 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
inviteeEmail: z.string().trim().email(),
|
inviteeEmails: z.array(z.string().trim().email()),
|
||||||
organizationId: z.string().trim()
|
organizationId: z.string().trim(),
|
||||||
|
projectIds: z.array(z.string().trim()).optional(),
|
||||||
|
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
|
||||||
|
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
completeInviteLink: z.string().optional()
|
completeInviteLinks: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
email: z.string(),
|
||||||
|
link: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
if (req.auth.actor !== ActorType.USER) return;
|
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,
|
orgId: req.body.organizationId,
|
||||||
userId: req.permission.id,
|
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,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
@@ -41,14 +55,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: PostHogEventTypes.UserOrgInvitation,
|
event: PostHogEventTypes.UserOrgInvitation,
|
||||||
distinctId: getTelemetryDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
inviteeEmail: req.body.inviteeEmail,
|
inviteeEmails: req.body.inviteeEmails,
|
||||||
|
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||||
...req.auditLogInfo
|
...req.auditLogInfo
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
completeInviteLink,
|
completeInviteLinks,
|
||||||
message: `Send an invite link to ${req.body.inviteeEmail}`
|
message: `Send an invite link to ${req.body.inviteeEmails.join(", ")}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,12 @@
|
|||||||
import { z } from "zod";
|
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 { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -59,12 +65,19 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
includeGroupMembers: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
}),
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
users: ProjectMembershipsSchema.extend({
|
users: ProjectMembershipsSchema.extend({
|
||||||
|
isGroupMember: z.boolean(),
|
||||||
user: UsersSchema.pick({
|
user: UsersSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
username: true,
|
username: true,
|
||||||
@@ -99,9 +112,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
includeGroupMembers: req.query.includeGroupMembers,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { users };
|
return { users };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -113,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
includeRoles: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.default("false")
|
||||||
|
.transform((value) => value === "true")
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
workspaces: projectWithEnv.array()
|
workspaces: projectWithEnv
|
||||||
|
.extend({
|
||||||
|
roles: ProjectRolesSchema.array().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
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 };
|
return { workspaces };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -179,7 +179,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyIV: z.string().trim(),
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim()
|
verifier: z.string().trim(),
|
||||||
|
tokenMetadata: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
125
backend/src/services/access-token-queue/access-token-queue.ts
Normal file
125
backend/src/services/access-token-queue/access-token-queue.ts
Normal file
@@ -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 {
|
export enum TokenType {
|
||||||
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
||||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||||
@@ -49,3 +51,19 @@ export type TIssueAuthTokenDTO = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
userAgent: 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 {
|
} else {
|
||||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||||
if (isLinkingRequired) {
|
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 { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
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 { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/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 { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
@@ -32,10 +35,14 @@ type TAuthSignupDep = {
|
|||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
userGroupMembershipDAL: Pick<
|
userGroupMembershipDAL: Pick<
|
||||||
TUserGroupMembershipDALFactory,
|
TUserGroupMembershipDALFactory,
|
||||||
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
|
| "find"
|
||||||
|
| "transaction"
|
||||||
|
| "insertMany"
|
||||||
|
| "deletePendingUserGroupMembershipsByUserIds"
|
||||||
|
| "findUserGroupMembershipsInProject"
|
||||||
>;
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||||
@@ -43,6 +50,8 @@ type TAuthSignupDep = {
|
|||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||||
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
||||||
@@ -58,6 +67,8 @@ export const authSignupServiceFactory = ({
|
|||||||
smtpService,
|
smtpService,
|
||||||
orgService,
|
orgService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
licenseService
|
licenseService
|
||||||
}: TAuthSignupDep) => {
|
}: TAuthSignupDep) => {
|
||||||
// first step of signup. create user and send email
|
// first step of signup. create user and send email
|
||||||
@@ -301,7 +312,8 @@ export const authSignupServiceFactory = ({
|
|||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
authorization
|
authorization,
|
||||||
|
tokenMetadata
|
||||||
}: TCompleteAccountInviteDTO) => {
|
}: TCompleteAccountInviteDTO) => {
|
||||||
const user = await userDAL.findUserByUsername(email);
|
const user = await userDAL.findUserByUsername(email);
|
||||||
if (!user || (user && user.isAccepted)) {
|
if (!user || (user && user.isAccepted)) {
|
||||||
@@ -358,6 +370,45 @@ export const authSignupServiceFactory = ({
|
|||||||
tx
|
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(
|
const updatedMembersips = await orgDAL.updateMembership(
|
||||||
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
||||||
{ userId: us.id, status: OrgMembershipStatus.Accepted },
|
{ userId: us.id, status: OrgMembershipStatus.Accepted },
|
||||||
|
@@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
authorization: string;
|
authorization: string;
|
||||||
|
tokenMetadata?: string;
|
||||||
};
|
};
|
||||||
|
@@ -13,6 +13,13 @@ import {
|
|||||||
TRebuildCaCrlDTO
|
TRebuildCaCrlDTO
|
||||||
} from "./certificate-authority-types";
|
} 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) => {
|
export const createDistinguishedName = (parts: TDNParts) => {
|
||||||
const dnParts = [];
|
const dnParts = [];
|
||||||
if (parts.country) dnParts.push(`C=${parts.country}`);
|
if (parts.country) dnParts.push(`C=${parts.country}`);
|
||||||
@@ -284,12 +291,11 @@ export const rebuildCaCrl = async ({
|
|||||||
thisUpdate: new Date(),
|
thisUpdate: new Date(),
|
||||||
nextUpdate: new Date("2025/12/12"),
|
nextUpdate: new Date("2025/12/12"),
|
||||||
entries: revokedCerts.map((revokedCert) => {
|
entries: revokedCerts.map((revokedCert) => {
|
||||||
|
const revocationDate = new Date(revokedCert.revokedAt as Date);
|
||||||
return {
|
return {
|
||||||
serialNumber: revokedCert.serialNumber,
|
serialNumber: revokedCert.serialNumber,
|
||||||
revocationDate: new Date(revokedCert.revokedAt as Date),
|
revocationDate,
|
||||||
reason: revokedCert.revocationReason as number,
|
reason: revokedCert.revocationReason as number
|
||||||
invalidity: new Date("2022/01/01"),
|
|
||||||
issuer: ca.dn
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
signingAlgorithm: alg,
|
signingAlgorithm: alg,
|
||||||
|
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|||||||
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-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 { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
import {
|
import {
|
||||||
createDistinguishedName,
|
createDistinguishedName,
|
||||||
|
createSerialNumber,
|
||||||
getCaCertChain, // TODO: consider rename
|
getCaCertChain, // TODO: consider rename
|
||||||
getCaCertChains,
|
getCaCertChains,
|
||||||
getCaCredentials,
|
getCaCredentials,
|
||||||
@@ -147,7 +149,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
? new Date(notAfter)
|
? new Date(notAfter)
|
||||||
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||||
|
|
||||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
const serialNumber = createSerialNumber();
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.create(
|
const ca = await certificateAuthorityDAL.create(
|
||||||
{
|
{
|
||||||
@@ -263,7 +265,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
await certificateAuthorityCrlDAL.create(
|
await certificateAuthorityCrlDAL.create(
|
||||||
{
|
{
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
encryptedCrl
|
encryptedCrl,
|
||||||
|
caSecretId: caSecret.id
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -368,7 +371,6 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
|
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
|
||||||
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" });
|
|
||||||
|
|
||||||
const { caPrivateKey, caPublicKey } = await getCaCredentials({
|
const { caPrivateKey, caPublicKey } = await getCaCredentials({
|
||||||
caId,
|
caId,
|
||||||
@@ -407,7 +409,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Renew certificate for CA with id [caId]
|
* Renew certificate for CA with id [caId]
|
||||||
* Note: Currently implements CA renewal with same key-pair only
|
* Note 1: This CA renewal method is only applicable to CAs with internal parent CAs
|
||||||
|
* Note 2: Currently implements CA renewal with same key-pair only
|
||||||
*/
|
*/
|
||||||
const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
|
const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
@@ -433,7 +436,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
// get latest CA certificate
|
// get latest CA certificate
|
||||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||||
|
|
||||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
const serialNumber = createSerialNumber();
|
||||||
|
|
||||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: ca.projectId,
|
projectId: ca.projectId,
|
||||||
@@ -846,7 +849,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
const serialNumber = createSerialNumber();
|
||||||
const intermediateCert = await x509.X509CertificateGenerator.create({
|
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
subject: csrObj.subject,
|
subject: csrObj.subject,
|
||||||
@@ -888,9 +891,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import certificate for (un-installed) CA with id [caId].
|
* Import certificate for CA with id [caId].
|
||||||
* Note: Can be used to import an external certificate and certificate chain
|
* Note: Can be used to import an external certificate and certificate chain
|
||||||
* to be installed into the CA.
|
* to be into an installed or uninstalled CA.
|
||||||
*/
|
*/
|
||||||
const importCertToCa = async ({
|
const importCertToCa = async ({
|
||||||
caId,
|
caId,
|
||||||
@@ -917,7 +920,18 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateAuthorities
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" });
|
if (ca.parentCaId) {
|
||||||
|
/**
|
||||||
|
* re-evaluate in the future if we should allow users to import a new CA certificate for an intermediate
|
||||||
|
* CA chained to an internal parent CA. Doing so would allow users to re-chain the CA to a different
|
||||||
|
* internal CA.
|
||||||
|
*/
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot import certificate to intermediate CA chained to internal parent CA"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const caCert = ca.activeCaCertId ? await certificateAuthorityCertDAL.findById(ca.activeCaCertId) : undefined;
|
||||||
|
|
||||||
const certObj = new x509.X509Certificate(certificate);
|
const certObj = new x509.X509Certificate(certificate);
|
||||||
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||||
@@ -988,7 +1002,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
encryptedCertificate,
|
encryptedCertificate,
|
||||||
encryptedCertificateChain,
|
encryptedCertificateChain,
|
||||||
version: 1,
|
version: caCert ? caCert.version + 1 : 1,
|
||||||
caSecretId: caSecret.id
|
caSecretId: caSecret.id
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@@ -1131,7 +1145,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||||
});
|
});
|
||||||
|
|
||||||
const { caPrivateKey } = await getCaCredentials({
|
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
@@ -1139,9 +1153,15 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
kmsService
|
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[] = [
|
const extensions: x509.Extension[] = [
|
||||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||||
new x509.BasicConstraintsExtension(false),
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
];
|
];
|
||||||
@@ -1192,7 +1212,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
const serialNumber = createSerialNumber();
|
||||||
const leafCert = await x509.X509CertificateGenerator.create({
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
subject: csrObj.subject,
|
subject: csrObj.subject,
|
||||||
@@ -1275,24 +1295,23 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
* Return new leaf certificate issued by CA with id [caId].
|
* Return new leaf certificate issued by CA with id [caId].
|
||||||
* Note: CSR is generated externally and submitted to Infisical.
|
* Note: CSR is generated externally and submitted to Infisical.
|
||||||
*/
|
*/
|
||||||
const signCertFromCa = async ({
|
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
||||||
caId,
|
|
||||||
certificateTemplateId,
|
|
||||||
csr,
|
|
||||||
pkiCollectionId,
|
|
||||||
friendlyName,
|
|
||||||
commonName,
|
|
||||||
altNames,
|
|
||||||
ttl,
|
|
||||||
notBefore,
|
|
||||||
notAfter,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actor,
|
|
||||||
actorOrgId
|
|
||||||
}: TSignCertFromCaDTO) => {
|
|
||||||
let ca: TCertificateAuthorities | undefined;
|
let ca: TCertificateAuthorities | undefined;
|
||||||
let certificateTemplate: TCertificateTemplates | undefined;
|
let certificateTemplate: TCertificateTemplates | undefined;
|
||||||
|
|
||||||
|
const {
|
||||||
|
caId,
|
||||||
|
certificateTemplateId,
|
||||||
|
csr,
|
||||||
|
pkiCollectionId,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
ttl,
|
||||||
|
notBefore,
|
||||||
|
notAfter
|
||||||
|
} = dto;
|
||||||
|
|
||||||
let collectionId = pkiCollectionId;
|
let collectionId = pkiCollectionId;
|
||||||
|
|
||||||
if (caId) {
|
if (caId) {
|
||||||
@@ -1313,15 +1332,20 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "CA not found" });
|
throw new BadRequestError({ message: "CA not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
if (!dto.isInternal) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actorId,
|
dto.actor,
|
||||||
ca.projectId,
|
dto.actorId,
|
||||||
actorAuthMethod,
|
ca.projectId,
|
||||||
actorOrgId
|
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.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
@@ -1362,6 +1386,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
notAfterDate = new Date(notAfter);
|
notAfterDate = new Date(notAfter);
|
||||||
} else if (ttl) {
|
} else if (ttl) {
|
||||||
notAfterDate = new Date(new Date().getTime() + ms(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);
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
@@ -1406,6 +1432,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let altNamesFromCsr: string = "";
|
||||||
let altNamesArray: {
|
let altNamesArray: {
|
||||||
type: "email" | "dns";
|
type: "email" | "dns";
|
||||||
value: string;
|
value: string;
|
||||||
@@ -1434,7 +1461,24 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
||||||
throw new Error(`Invalid altName: ${altName}`);
|
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);
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
extensions.push(altNamesExtension);
|
extensions.push(altNamesExtension);
|
||||||
}
|
}
|
||||||
@@ -1451,7 +1495,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serialNumber = crypto.randomBytes(32).toString("hex");
|
const serialNumber = createSerialNumber();
|
||||||
const leafCert = await x509.X509CertificateGenerator.create({
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
subject: csrObj.subject,
|
subject: csrObj.subject,
|
||||||
@@ -1480,7 +1524,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
status: CertStatus.ACTIVE,
|
status: CertStatus.ACTIVE,
|
||||||
friendlyName: friendlyName || csrObj.subject,
|
friendlyName: friendlyName || csrObj.subject,
|
||||||
commonName: cn,
|
commonName: cn,
|
||||||
altNames,
|
altNames: altNamesFromCsr || altNames,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
notBefore: notBeforeDate,
|
notBefore: notBeforeDate,
|
||||||
notAfter: notAfterDate
|
notAfter: notAfterDate
|
||||||
@@ -1518,7 +1562,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: leafCert.toString("pem"),
|
certificate: leafCert,
|
||||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||||
issuingCaCertificate,
|
issuingCaCertificate,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
|
@@ -97,18 +97,33 @@ export type TIssueCertFromCaDTO = {
|
|||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TSignCertFromCaDTO = {
|
export type TSignCertFromCaDTO =
|
||||||
caId?: string;
|
| {
|
||||||
csr: string;
|
isInternal: true;
|
||||||
certificateTemplateId?: string;
|
caId?: string;
|
||||||
pkiCollectionId?: string;
|
csr: string;
|
||||||
friendlyName?: string;
|
certificateTemplateId?: string;
|
||||||
commonName?: string;
|
pkiCollectionId?: string;
|
||||||
altNames: string;
|
friendlyName?: string;
|
||||||
ttl: string;
|
commonName?: string;
|
||||||
notBefore?: string;
|
altNames?: string;
|
||||||
notAfter?: string;
|
ttl?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
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 = {
|
export type TDNParts = {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
|
24
backend/src/services/certificate-est/certificate-est-fns.ts
Normal file
24
backend/src/services/certificate-est/certificate-est-fns.ts
Normal file
@@ -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;
|
||||||
|
};
|
231
backend/src/services/certificate-est/certificate-est-service.ts
Normal file
231
backend/src/services/certificate-est/certificate-est-service.ts
Normal file
@@ -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 { 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
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 { TCertificateTemplateDALFactory } from "./certificate-template-dal";
|
||||||
|
import { TCertificateTemplateEstConfigDALFactory } from "./certificate-template-est-config-dal";
|
||||||
import {
|
import {
|
||||||
TCreateCertTemplateDTO,
|
TCreateCertTemplateDTO,
|
||||||
|
TCreateEstConfigurationDTO,
|
||||||
TDeleteCertTemplateDTO,
|
TDeleteCertTemplateDTO,
|
||||||
TGetCertTemplateDTO,
|
TGetCertTemplateDTO,
|
||||||
TUpdateCertTemplateDTO
|
TGetEstConfigurationDTO,
|
||||||
|
TUpdateCertTemplateDTO,
|
||||||
|
TUpdateEstConfigurationDTO
|
||||||
} from "./certificate-template-types";
|
} from "./certificate-template-types";
|
||||||
|
|
||||||
type TCertificateTemplateServiceFactoryDep = {
|
type TCertificateTemplateServiceFactoryDep = {
|
||||||
certificateTemplateDAL: TCertificateTemplateDALFactory;
|
certificateTemplateDAL: TCertificateTemplateDALFactory;
|
||||||
|
certificateTemplateEstConfigDAL: TCertificateTemplateEstConfigDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
@@ -23,8 +38,11 @@ export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTe
|
|||||||
|
|
||||||
export const certificateTemplateServiceFactory = ({
|
export const certificateTemplateServiceFactory = ({
|
||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
|
certificateTemplateEstConfigDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
}: TCertificateTemplateServiceFactoryDep) => {
|
}: TCertificateTemplateServiceFactoryDep) => {
|
||||||
const createCertTemplate = async ({
|
const createCertTemplate = async ({
|
||||||
caId,
|
caId,
|
||||||
@@ -187,10 +205,228 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
return certTemplate;
|
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 {
|
return {
|
||||||
createCertTemplate,
|
createCertTemplate,
|
||||||
getCertTemplate,
|
getCertTemplate,
|
||||||
deleteCertTemplate,
|
deleteCertTemplate,
|
||||||
updateCertTemplate
|
updateCertTemplate,
|
||||||
|
createEstConfiguration,
|
||||||
|
updateEstConfiguration,
|
||||||
|
getEstConfiguration
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -26,3 +26,27 @@ export type TGetCertTemplateDTO = {
|
|||||||
export type TDeleteCertTemplateDTO = {
|
export type TDeleteCertTemplateDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & 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;
|
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 { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
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 { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
|
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
|
||||||
|
|
||||||
|
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
|
||||||
import { AuthTokenType } from "../auth/auth-type";
|
import { AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
|
||||||
@@ -13,19 +14,24 @@ import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity
|
|||||||
type TIdentityAccessTokenServiceFactoryDep = {
|
type TIdentityAccessTokenServiceFactoryDep = {
|
||||||
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
|
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
|
accessTokenQueue: Pick<
|
||||||
|
TAccessTokenQueueServiceFactory,
|
||||||
|
"updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache"
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
|
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
|
||||||
|
|
||||||
export const identityAccessTokenServiceFactory = ({
|
export const identityAccessTokenServiceFactory = ({
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityOrgMembershipDAL
|
identityOrgMembershipDAL,
|
||||||
|
accessTokenQueue
|
||||||
}: TIdentityAccessTokenServiceFactoryDep) => {
|
}: TIdentityAccessTokenServiceFactoryDep) => {
|
||||||
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
|
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
|
||||||
const {
|
const {
|
||||||
id: tokenId,
|
id: tokenId,
|
||||||
accessTokenTTL,
|
|
||||||
accessTokenNumUses,
|
accessTokenNumUses,
|
||||||
|
accessTokenTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenLastRenewedAt,
|
accessTokenLastRenewedAt,
|
||||||
createdAt: accessTokenCreatedAt
|
createdAt: accessTokenCreatedAt
|
||||||
@@ -83,7 +89,12 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (!identityAccessToken) throw new UnauthorizedError();
|
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;
|
const { accessTokenMaxTTL, createdAt: accessTokenCreatedAt, accessTokenTTL } = identityAccessToken;
|
||||||
|
|
||||||
@@ -164,14 +175,14 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
|
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, {
|
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
|
||||||
accessTokenLastUsedAt: new Date(),
|
|
||||||
$incr: {
|
|
||||||
accessTokenNumUses: 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
|
return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -65,7 +65,7 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}: { data: TGetCallerIdentityResponse } = await axios({
|
}: { data: TGetCallerIdentityResponse } = await axios({
|
||||||
method: iamHttpRequestMethod,
|
method: iamHttpRequestMethod,
|
||||||
url: identityAwsAuth.stsEndpoint,
|
url: headers?.Host ? `https://${headers.Host}` : identityAwsAuth.stsEndpoint,
|
||||||
headers,
|
headers,
|
||||||
data: body
|
data: body
|
||||||
});
|
});
|
||||||
|
@@ -1030,11 +1030,31 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
return apps;
|
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 ({
|
export const getApps = async ({
|
||||||
integration,
|
integration,
|
||||||
accessToken,
|
accessToken,
|
||||||
accessId,
|
accessId,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
url
|
url
|
||||||
}: {
|
}: {
|
||||||
@@ -1042,6 +1062,7 @@ export const getApps = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessId?: string;
|
accessId?: string;
|
||||||
teamId?: string | null;
|
teamId?: string | null;
|
||||||
|
azureDevOpsOrgName?: string | null;
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
}): Promise<App[]> => {
|
}): Promise<App[]> => {
|
||||||
@@ -1184,6 +1205,12 @@ export const getApps = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case Integrations.AZURE_DEVOPS:
|
||||||
|
return getAppsAzureDevOps({
|
||||||
|
accessToken,
|
||||||
|
orgName: azureDevOpsOrgName as string
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({ message: "integration not found" });
|
throw new BadRequestError({ message: "integration not found" });
|
||||||
}
|
}
|
||||||
|
@@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
id,
|
id,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
}: TIntegrationAuthAppsDTO) => {
|
}: TIntegrationAuthAppsDTO) => {
|
||||||
@@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
accessToken,
|
accessToken,
|
||||||
accessId,
|
accessId,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
url: integrationAuth.url
|
url: integrationAuth.url
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { TIntegrations } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetIntegrationAuthDTO = {
|
export type TGetIntegrationAuthDTO = {
|
||||||
@@ -28,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
|
|||||||
export type TIntegrationAuthAppsDTO = {
|
export type TIntegrationAuthAppsDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
|
azureDevOpsOrgName?: string;
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
@@ -163,3 +165,13 @@ export type TTeamCityBuildConfig = {
|
|||||||
href: string;
|
href: string;
|
||||||
webUrl: 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",
|
CLOUD_66 = "cloud-66",
|
||||||
NORTHFLANK = "northflank",
|
NORTHFLANK = "northflank",
|
||||||
HASURA_CLOUD = "hasura-cloud",
|
HASURA_CLOUD = "hasura-cloud",
|
||||||
RUNDECK = "rundeck"
|
RUNDECK = "rundeck",
|
||||||
|
AZURE_DEVOPS = "azure-devops"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IntegrationType {
|
export enum IntegrationType {
|
||||||
@@ -88,6 +89,7 @@ export enum IntegrationUrls {
|
|||||||
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
||||||
NORTHFLANK_API_URL = "https://api.northflank.com",
|
NORTHFLANK_API_URL = "https://api.northflank.com",
|
||||||
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
|
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_SERVICE_NAME = "secretmanager.googleapis.com",
|
||||||
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||||
@@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => {
|
|||||||
type: "pat",
|
type: "pat",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
docsLink: ""
|
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 { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||||
|
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
|
||||||
import {
|
import {
|
||||||
IntegrationInitialSyncBehavior,
|
IntegrationInitialSyncBehavior,
|
||||||
IntegrationMappingBehavior,
|
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]
|
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||||
*/
|
*/
|
||||||
@@ -3714,6 +3825,15 @@ export const syncIntegrationSecrets = async ({
|
|||||||
updateManySecretsRawFn
|
updateManySecretsRawFn
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Integrations.AZURE_DEVOPS:
|
||||||
|
await syncSecretsAzureDevops({
|
||||||
|
integrationAuth,
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
case Integrations.AWS_PARAMETER_STORE:
|
case Integrations.AWS_PARAMETER_STORE:
|
||||||
response = await syncSecretsAWSParameterStore({
|
response = await syncSecretsAWSParameterStore({
|
||||||
integration,
|
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 {
|
try {
|
||||||
const members = await db
|
const conn = tx || db;
|
||||||
.replicaNode()(TableName.OrgMembership)
|
const members = await conn(TableName.OrgMembership)
|
||||||
|
// .replicaNode()(TableName.OrgMembership)
|
||||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.leftJoin<TUserEncryptionKeys>(
|
.leftJoin<TUserEncryptionKeys>(
|
||||||
@@ -126,18 +127,18 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.Users}.id`
|
`${TableName.Users}.id`
|
||||||
)
|
)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.OrgMembership),
|
conn.ref("id").withSchema(TableName.OrgMembership),
|
||||||
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||||
db.ref("orgId").withSchema(TableName.OrgMembership),
|
conn.ref("orgId").withSchema(TableName.OrgMembership),
|
||||||
db.ref("role").withSchema(TableName.OrgMembership),
|
conn.ref("role").withSchema(TableName.OrgMembership),
|
||||||
db.ref("roleId").withSchema(TableName.OrgMembership),
|
conn.ref("roleId").withSchema(TableName.OrgMembership),
|
||||||
db.ref("status").withSchema(TableName.OrgMembership),
|
conn.ref("status").withSchema(TableName.OrgMembership),
|
||||||
db.ref("username").withSchema(TableName.Users),
|
conn.ref("username").withSchema(TableName.Users),
|
||||||
db.ref("email").withSchema(TableName.Users),
|
conn.ref("email").withSchema(TableName.Users),
|
||||||
db.ref("firstName").withSchema(TableName.Users),
|
conn.ref("firstName").withSchema(TableName.Users),
|
||||||
db.ref("lastName").withSchema(TableName.Users),
|
conn.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
conn.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
)
|
)
|
||||||
.where({ isGhost: false })
|
.where({ isGhost: false })
|
||||||
.whereIn("username", usernames);
|
.whereIn("username", usernames);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
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 { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
@@ -12,6 +13,7 @@ type TDeleteOrgMembership = {
|
|||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||||
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteOrgMembershipFn = async ({
|
export const deleteOrgMembershipFn = async ({
|
||||||
@@ -19,6 +21,7 @@ export const deleteOrgMembershipFn = async ({
|
|||||||
orgId,
|
orgId,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
licenseService
|
licenseService
|
||||||
@@ -39,6 +42,13 @@ export const deleteOrgMembershipFn = async ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectUserAdditionalPrivilegeDAL.delete(
|
||||||
|
{
|
||||||
|
userId: orgMembership.userId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
// Get all the project memberships of the user in the organization
|
// Get all the project memberships of the user in the organization
|
||||||
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
|
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
|
||||||
|
|
||||||
|
@@ -4,12 +4,21 @@ import crypto from "crypto";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { Knex } from "knex";
|
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 { TProjects } from "@app/db/schemas/projects";
|
||||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
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 { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
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 { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
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 { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||||
@@ -55,8 +68,11 @@ type TOrgServiceFactoryDep = {
|
|||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
groupDAL: TGroupDALFactory;
|
groupDAL: TGroupDALFactory;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
|
projectMembershipDAL: Pick<
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
TProjectMembershipDALFactory,
|
||||||
|
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||||
|
>;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
|
||||||
incidentContactDAL: TIncidentContactsDALFactory;
|
incidentContactDAL: TIncidentContactsDALFactory;
|
||||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||||
@@ -67,6 +83,10 @@ type TOrgServiceFactoryDep = {
|
|||||||
TLicenseServiceFactory,
|
TLicenseServiceFactory,
|
||||||
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
"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>;
|
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||||
@@ -84,10 +104,14 @@ export const orgServiceFactory = ({
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
samlConfigDAL
|
samlConfigDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectUserMembershipRoleDAL
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
@@ -417,10 +441,15 @@ export const orgServiceFactory = ({
|
|||||||
const inviteUserToOrganization = async ({
|
const inviteUserToOrganization = async ({
|
||||||
orgId,
|
orgId,
|
||||||
userId,
|
userId,
|
||||||
inviteeEmail,
|
inviteeEmails,
|
||||||
|
organizationRoleSlug,
|
||||||
|
projectRoleSlug,
|
||||||
|
projectIds,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TInviteUserToOrgDTO) => {
|
}: TInviteUserToOrgDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
@@ -447,98 +476,203 @@ export const orgServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitee = await orgDAL.transaction(async (tx) => {
|
if (projectIds?.length) {
|
||||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
const projects = await projectDAL.find({
|
||||||
if (inviteeUser) {
|
orgId,
|
||||||
// if user already exist means its already part of infisical
|
$in: {
|
||||||
// Thus the signup flow is not needed anymore
|
id: projectIds
|
||||||
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 (!inviteeMembership) {
|
// if its not v3, throw an error
|
||||||
await orgDAL.createMembership(
|
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
|
||||||
{
|
|
||||||
userId: inviteeUser.id,
|
|
||||||
inviteEmail: inviteeEmail,
|
|
||||||
orgId,
|
|
||||||
role: OrgMembershipRole.Member,
|
|
||||||
status: OrgMembershipStatus.Invited,
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return inviteeUser;
|
|
||||||
}
|
|
||||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
|
||||||
if (isEmailInvalid) {
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Provided a disposable email",
|
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||||
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: OrgMembershipRole.Member,
|
|
||||||
status: OrgMembershipStatus.Invited,
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await tokenService.createTokenForUser({
|
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
||||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
const users: Pick<
|
||||||
userId: invitee.id,
|
TUsers & { orgId: string },
|
||||||
orgId
|
"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 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);
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
|
|
||||||
if (!appCfg.isSmtpConfigured) {
|
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,
|
orgId,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
licenseService
|
licenseService
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
@@ -29,7 +30,10 @@ export type TInviteUserToOrgDTO = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
inviteeEmail: string;
|
inviteeEmails: string[];
|
||||||
|
organizationRoleSlug: OrgMembershipRole;
|
||||||
|
projectIds?: string[];
|
||||||
|
projectRoleSlug?: ProjectMembershipRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyUserToOrgDTO = {
|
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 { ForbiddenError } from "@casl/ability";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import {
|
import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
|
||||||
ProjectMembershipRole,
|
|
||||||
ProjectVersion,
|
|
||||||
SecretKeyEncoding,
|
|
||||||
TableName,
|
|
||||||
TProjectMemberships
|
|
||||||
} from "@app/db/schemas";
|
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||||
|
import { addMembersToProject } from "./project-membership-fns";
|
||||||
import {
|
import {
|
||||||
ProjectUserMembershipTemporaryMode,
|
ProjectUserMembershipTemporaryMode,
|
||||||
TAddUsersToWorkspaceDTO,
|
TAddUsersToWorkspaceDTO,
|
||||||
@@ -51,9 +46,11 @@ type TProjectMembershipServiceFactoryDep = {
|
|||||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
|
groupProjectDAL: TGroupProjectDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
||||||
@@ -66,8 +63,10 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
groupProjectDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
licenseService
|
licenseService
|
||||||
@@ -77,6 +76,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
|
includeGroupMembers,
|
||||||
projectId
|
projectId
|
||||||
}: TGetProjectMembershipDTO) => {
|
}: TGetProjectMembershipDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@@ -88,7 +88,25 @@ export const projectMembershipServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
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 ({
|
const getProjectMembershipByUsername = async ({
|
||||||
@@ -222,116 +240,23 @@ export const projectMembershipServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
const usernamesAndEmails = [...emails, ...usernames];
|
const members = await addMembersToProject({
|
||||||
|
orgDAL,
|
||||||
const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [
|
projectDAL,
|
||||||
...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))
|
projectMembershipDAL,
|
||||||
]);
|
projectKeyDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
if (orgMembers.length !== usernamesAndEmails.length)
|
projectBotDAL,
|
||||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
projectUserMembershipRoleDAL,
|
||||||
|
smtpService
|
||||||
if (!orgMembers.length) return [];
|
}).addMembersToNonE2EEProject({
|
||||||
|
emails,
|
||||||
const existingMembers = await projectMembershipDAL.find({
|
usernames,
|
||||||
projectId,
|
projectId,
|
||||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
projectMembershipRole: ProjectMembershipRole.Member,
|
||||||
});
|
sendEmails
|
||||||
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: 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;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -502,6 +427,16 @@ export const projectMembershipServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const memberships = await projectMembershipDAL.transaction(async (tx) => {
|
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(
|
const deletedMemberships = await projectMembershipDAL.delete(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
@@ -564,12 +499,25 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedMembership = (
|
const deletedMembership = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
await projectMembershipDAL.delete({
|
await projectUserAdditionalPrivilegeDAL.delete(
|
||||||
projectId: project.id,
|
{
|
||||||
userId: actorId
|
projectId: project.id,
|
||||||
})
|
userId: actorId
|
||||||
)?.[0];
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const membership = (
|
||||||
|
await projectMembershipDAL.delete(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
userId: actorId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
)?.[0];
|
||||||
|
return membership;
|
||||||
|
});
|
||||||
|
|
||||||
if (!deletedMembership) {
|
if (!deletedMembership) {
|
||||||
throw new BadRequestError({ message: "Failed to leave project" });
|
throw new BadRequestError({ message: "Failed to leave project" });
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean } & TProjectPermission;
|
||||||
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
|
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
|
||||||
export enum ProjectUserMembershipTemporaryMode {
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
Relative = "relative"
|
Relative = "relative"
|
||||||
|
52
backend/src/services/project-role/project-role-fns.ts
Normal file
52
backend/src/services/project-role/project-role-fns.ts
Normal file
@@ -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 { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
projectAdminPermissions,
|
|
||||||
projectMemberPermissions,
|
|
||||||
projectNoAccessPermissions,
|
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSet,
|
ProjectPermissionSet,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub
|
||||||
projectViewerPermission
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
@@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||||
|
import { getPredefinedRoles } from "./project-role-fns";
|
||||||
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
||||||
|
|
||||||
type TProjectRoleServiceFactoryDep = {
|
type TProjectRoleServiceFactoryDep = {
|
||||||
@@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
|
|||||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
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 = ({
|
export const projectRoleServiceFactory = ({
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
permissionService,
|
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 {
|
return {
|
||||||
...projectOrm,
|
...projectOrm,
|
||||||
findAllProjects,
|
findAllProjects,
|
||||||
@@ -288,6 +316,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
findProjectById,
|
findProjectById,
|
||||||
findProjectByFilter,
|
findProjectByFilter,
|
||||||
findProjectBySlug,
|
findProjectBySlug,
|
||||||
|
findProjectWithOrg,
|
||||||
checkProjectUpgradeStatus
|
checkProjectUpgradeStatus
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { ProjectVersion, TProjects } from "@app/db/schemas";
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
|
|||||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
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 ({
|
export const getProjectKmsCertificateKeyId = async ({
|
||||||
projectId,
|
projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
@@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
|
|||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
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 { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
|
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
@@ -44,6 +47,7 @@ import {
|
|||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
TListProjectCertsDTO,
|
TListProjectCertsDTO,
|
||||||
|
TListProjectsDTO,
|
||||||
TLoadProjectKmsBackupDTO,
|
TLoadProjectKmsBackupDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
TUpdateAuditLogsRetentionDTO,
|
TUpdateAuditLogsRetentionDTO,
|
||||||
@@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||||
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
TKmsServiceFactory,
|
TKmsServiceFactory,
|
||||||
| "updateProjectSecretManagerKmsKey"
|
| "updateProjectSecretManagerKmsKey"
|
||||||
@@ -112,6 +117,7 @@ export const projectServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
|
projectRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
@@ -389,8 +395,34 @@ export const projectServiceFactory = ({
|
|||||||
return deletedProject;
|
return deletedProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjects = async (actorId: string) => {
|
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
|
||||||
const workspaces = await projectDAL.findAllProjects(actorId);
|
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;
|
return workspaces;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
|
|||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListProjectsDTO = {
|
||||||
|
includeRoles: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpgradeProjectDTO = {
|
export type TUpgradeProjectDTO = {
|
||||||
userPrivateKey: string;
|
userPrivateKey: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
@@ -8,6 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
@@ -26,6 +27,7 @@ type TServiceTokenServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
||||||
@@ -35,7 +37,8 @@ export const serviceTokenServiceFactory = ({
|
|||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
accessTokenQueue
|
||||||
}: TServiceTokenServiceFactoryDep) => {
|
}: TServiceTokenServiceFactoryDep) => {
|
||||||
const createServiceToken = async ({
|
const createServiceToken = async ({
|
||||||
iv,
|
iv,
|
||||||
@@ -166,11 +169,9 @@ export const serviceTokenServiceFactory = ({
|
|||||||
|
|
||||||
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash);
|
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash);
|
||||||
if (!isMatch) throw new UnauthorizedError();
|
if (!isMatch) throw new UnauthorizedError();
|
||||||
const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, {
|
await accessTokenQueue.updateServiceTokenStatus(serviceToken.id);
|
||||||
lastUsed: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId };
|
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -25,6 +25,7 @@ export enum SmtpTemplates {
|
|||||||
UnlockAccount = "unlockAccount.handlebars",
|
UnlockAccount = "unlockAccount.handlebars",
|
||||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||||
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
|
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
|
||||||
|
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview.handlebars",
|
||||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<h2>Join your organization on Infisical</h2>
|
<h2>Join your organization on Infisical</h2>
|
||||||
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p>
|
<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>
|
<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>
|
<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>
|
</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 = {
|
export type TUserOrgInvitedEvent = {
|
||||||
event: PostHogEventTypes.UserOrgInvitation;
|
event: PostHogEventTypes.UserOrgInvitation;
|
||||||
properties: {
|
properties: {
|
||||||
inviteeEmail: string;
|
inviteeEmails: string[];
|
||||||
|
projectIds?: string[];
|
||||||
|
organizationRoleSlug?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -152,6 +153,28 @@ var loginCmd = &cobra.Command{
|
|||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
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{
|
infisicalClient := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{
|
||||||
SiteUrl: config.INFISICAL_URL,
|
SiteUrl: config.INFISICAL_URL,
|
||||||
UserAgent: api.USER_AGENT,
|
UserAgent: api.USER_AGENT,
|
||||||
@@ -464,6 +487,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(loginCmd)
|
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().BoolP("interactive", "i", false, "login via the command line")
|
||||||
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
|
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
|
||||||
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
|
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
|
||||||
@@ -499,10 +523,12 @@ func DomainOverridePrompt() (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func askForDomain() 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 (
|
const (
|
||||||
INFISICAL_CLOUD = "Infisical Cloud"
|
INFISICAL_CLOUD = "Infisical Cloud"
|
||||||
SELF_HOSTING = "Self Hosting"
|
SELF_HOSTING = "Self Hosting"
|
||||||
|
ADD_NEW_DOMAIN = "Add a new domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
options := []string{INFISICAL_CLOUD, SELF_HOSTING}
|
options := []string{INFISICAL_CLOUD, SELF_HOSTING}
|
||||||
@@ -524,6 +550,36 @@ func askForDomain() error {
|
|||||||
return nil
|
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 {
|
urlValidation := func(input string) error {
|
||||||
_, err := url.ParseRequestURI(input)
|
_, err := url.ParseRequestURI(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -542,12 +598,23 @@ func askForDomain() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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, "/")
|
domain = strings.TrimRight(domain, "/")
|
||||||
//set api and login url
|
|
||||||
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
|
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
|
||||||
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,6 +16,7 @@ type ConfigFile struct {
|
|||||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||||
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
||||||
|
Domains []string `json:"domains,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoggedInUser struct {
|
type LoggedInUser struct {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user