mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-22 13:29:55 +00:00
Compare commits
399 Commits
misc/updat
...
revert-249
Author | SHA1 | Date | |
---|---|---|---|
4090c894fc | |||
d430293c66 | |||
180d2692cd | |||
433e58655a | |||
5ffb6b7232 | |||
55ca9149d5 | |||
4ea57ca9a0 | |||
7ac4b0b79f | |||
2d51ed317f | |||
02c51b05b6 | |||
441b008709 | |||
4d81a0251e | |||
59da513481 | |||
c17047a193 | |||
f50a881273 | |||
c011d99b8b | |||
adc3542750 | |||
82e3241f1b | |||
2bca46886a | |||
971987c786 | |||
cd71a13bb7 | |||
98290fe31b | |||
9f15fb1474 | |||
301a867f8b | |||
658a044e85 | |||
2c1e29445d | |||
3f4c4f7418 | |||
592cc13b1f | |||
e70c2f3d10 | |||
bac865eab1 | |||
3d8fbc0a58 | |||
a1f2629366 | |||
bf8e1f2bfd | |||
f7d10ceeda | |||
095883a94e | |||
51638b7c71 | |||
adaddad370 | |||
cf6ff58f16 | |||
3e3f42a8f7 | |||
974e21d856 | |||
da86338bfe | |||
3a9a6767a0 | |||
fe8a1e6ce6 | |||
55aa3f7b58 | |||
59f3581370 | |||
ccae63936c | |||
6733349af0 | |||
50b51f1810 | |||
fc39b3b0dd | |||
5964976e47 | |||
677a87150b | |||
2469c8d0c6 | |||
dafb89d1dd | |||
8da01445e5 | |||
6b2273d314 | |||
b886e66ee9 | |||
3afcb19727 | |||
06d2480f30 | |||
fd7d8ddf2d | |||
1dc0f4e5b8 | |||
fa64a88c24 | |||
385ec05e57 | |||
3a38e1e413 | |||
7f04e9e97d | |||
839f0c7e1c | |||
2352e29902 | |||
fcbc7fcece | |||
c2252c65a4 | |||
e150673de4 | |||
4f5c49a529 | |||
7107089ad3 | |||
967818f57d | |||
14c89c9be5 | |||
02111c2dc2 | |||
ebea74b607 | |||
5bbe5421bf | |||
279289989f | |||
bb4a16cf7c | |||
309db49f1b | |||
62a582ef17 | |||
d6b389760d | |||
bd4deb02b0 | |||
449e7672f9 | |||
31ff6d3c17 | |||
cfcc32271f | |||
e2ea84f28a | |||
6885ef2e54 | |||
8fa9f476e3 | |||
1cf8d1e3fa | |||
9f61177b62 | |||
59b8e83476 | |||
eee4d00a08 | |||
51c0598b50 | |||
69311f058b | |||
0f70c3ea9a | |||
b5660c87a0 | |||
2a686e65cd | |||
2bb0386220 | |||
526605a0bb | |||
5b9903a226 | |||
3fc60bf596 | |||
7815d6538f | |||
4c4d525655 | |||
e44213a8a9 | |||
e87656631c | |||
e102ccf9f0 | |||
63af75a330 | |||
8a10af9b62 | |||
18308950d1 | |||
86a9676a9c | |||
aa12a71ff3 | |||
aee46d1902 | |||
279a1791f6 | |||
8d71b295ea | |||
f72cedae10 | |||
864cf23416 | |||
10574bfe26 | |||
02085ce902 | |||
4eeea0b27c | |||
93b7f56337 | |||
12ecefa832 | |||
dd9a00679d | |||
081502848d | |||
0fa9fa20bc | |||
0a1f25a659 | |||
bc74c44f97 | |||
c50e325f53 | |||
0225e6fabb | |||
3caa46ade8 | |||
998bbe92f7 | |||
009be0ded8 | |||
c9f6207e32 | |||
36adc5e00e | |||
cb24b2aac8 | |||
1e0eb26dce | |||
f8161c8c72 | |||
862e2e9d65 | |||
0e734bd638 | |||
a35054f6ba | |||
e0ace85d6e | |||
7867587884 | |||
0564d06923 | |||
8ace72d134 | |||
491331e9e3 | |||
4a324eafd8 | |||
173cf0238d | |||
fd792e7e1d | |||
d0656358a2 | |||
040fa511f6 | |||
75099f159f | |||
e4a83ad2e2 | |||
760f9d487c | |||
a02e73e2a4 | |||
d6b7045461 | |||
bd9c9ea1f4 | |||
d4c95ab1a7 | |||
fbebeaf38f | |||
97245c740e | |||
03c4c2056a | |||
cee982754b | |||
a6497b844a | |||
788dcf2c73 | |||
6d9f80805e | |||
7f055450df | |||
9234213c62 | |||
5a40b5a1cf | |||
19e4a6de4d | |||
0daca059c7 | |||
e7278c4cd9 | |||
3e79dbb3f5 | |||
0fd193f8e0 | |||
342c713805 | |||
9b2565e387 | |||
1c5a8cabe9 | |||
613b97c93d | |||
335f3f7d37 | |||
5740d2b4e4 | |||
b3f0d36ddc | |||
09887a7405 | |||
38ee3a005e | |||
10e7999334 | |||
8c458588ab | |||
2381a2e4ba | |||
9ef8812205 | |||
37a204e49e | |||
11927f341a | |||
6fc17a4964 | |||
eb00232db6 | |||
4fd245e493 | |||
d92c57d051 | |||
beaef1feb0 | |||
033fd5e7a4 | |||
f49f3c926c | |||
280d44f1e5 | |||
4eea0dc544 | |||
8a33f1a591 | |||
74653e7ed1 | |||
56ff11d63f | |||
1ecce285f0 | |||
b5c9b6a1bd | |||
e12ac6c07e | |||
dbb8617180 | |||
8a0b1bb427 | |||
1f6faadf81 | |||
8f3b7e1698 | |||
24c460c695 | |||
8acceab1e7 | |||
d60aba9339 | |||
3a228f7521 | |||
3f7ac0f142 | |||
63cf535ebb | |||
69a2a46c47 | |||
d081077273 | |||
75034f9350 | |||
eacd7b0c6a | |||
5bad77083c | |||
ea480c222b | |||
1fb644af4a | |||
a6f4a95821 | |||
8578208f2d | |||
fc4189ba0f | |||
b9ecf42fb6 | |||
008e18638f | |||
ac3b9c25dd | |||
f4997dec12 | |||
fcf405c630 | |||
efc6876260 | |||
1025759efb | |||
8bab6d87bb | |||
39a49f12f5 | |||
cfd841ea08 | |||
4d67c03e3e | |||
8826bc5d60 | |||
03fdce67f1 | |||
72f3f7980e | |||
f1aa2fbd84 | |||
217de6250f | |||
f742bd01d9 | |||
3fe53d5183 | |||
a5f5f803df | |||
c37e3ba635 | |||
55279e5e41 | |||
88fb37e8c6 | |||
6271dcc25d | |||
0f7faa6bfe | |||
4ace339d5b | |||
e8c0d1ece9 | |||
bb1977976c | |||
bb3da75870 | |||
088e888560 | |||
180241fdf0 | |||
93f27a7ee8 | |||
ed3bc8dd27 | |||
8dc4809ec8 | |||
a55d64e430 | |||
02d54da74a | |||
d660168700 | |||
1c75fc84f0 | |||
f63da87c7f | |||
53b9fe2dec | |||
87dc0eed7e | |||
f2dd6f94a4 | |||
ac26ae3893 | |||
4c65e9910a | |||
5e5ab29ab9 | |||
5150c102e6 | |||
41c29d41e1 | |||
4de33190a9 | |||
7cfecb39e4 | |||
7524b83c29 | |||
7a41cdf51b | |||
17d99cb2cf | |||
bd0da0ff74 | |||
d2a54234f4 | |||
626262461a | |||
93ba29e57f | |||
1581aa088d | |||
ceab951bca | |||
2e3dcc50ae | |||
a79087670e | |||
7b04c08fc7 | |||
70842b8e5e | |||
36e3e4c1b5 | |||
ce9b66ef14 | |||
1384c8e855 | |||
f213c75ede | |||
6ade708e19 | |||
ce3af41ebc | |||
e442f10fa5 | |||
2e8ad18285 | |||
f03ca7f916 | |||
bfa533e9d2 | |||
a8759e7410 | |||
af1905a39e | |||
16182a9d1d | |||
1321aa712f | |||
c1f61f2db4 | |||
5ad00130ea | |||
ea5e8e29e6 | |||
e7f89bdfef | |||
d23a7e41f3 | |||
52a885716d | |||
3fc907f076 | |||
eaf10483c0 | |||
dcd0234fb5 | |||
4dda270e8e | |||
4e6b289e1b | |||
c1cb85b49f | |||
ed71e651f6 | |||
6fab7d9507 | |||
1a11dd954b | |||
5d3574d3f6 | |||
aa42aa05aa | |||
7a36badb23 | |||
9ce6fd3f8e | |||
a549c8b9e3 | |||
1c749c84f2 | |||
1bc1feb843 | |||
80ca115ccd | |||
5a6bb90870 | |||
de7a693a6a | |||
096417281e | |||
763a96faf8 | |||
870eaf9301 | |||
10abf192a1 | |||
508f697bdd | |||
8ea8a6f72e | |||
54e6f4b607 | |||
ea3b3c5cec | |||
a8fd83652d | |||
45f3675337 | |||
87a9a87dcd | |||
0b882ece8c | |||
e005e94165 | |||
0e07eaaa01 | |||
e10e313af3 | |||
e6c0bbb25b | |||
2b39d9e6c4 | |||
cf42279e5b | |||
4baa6b1d3d | |||
a410d560a7 | |||
99e150cc1d | |||
e7191c2f71 | |||
f6deb0969a | |||
1163e41e64 | |||
a0f93f995e | |||
50fcf97a36 | |||
8e68d21115 | |||
372b6cbaea | |||
26add7bfd1 | |||
f3d207ab5c | |||
e1cd632546 | |||
655ee4f118 | |||
34a2452bf5 | |||
7846a81636 | |||
6bdf3455f5 | |||
556ae168dd | |||
7b19d2aa6a | |||
bda9bb3d61 | |||
4b66a9343c | |||
4930d7fc02 | |||
ad644db512 | |||
ffaf145317 | |||
17b0d0081d | |||
ecf177fecc | |||
6112bc9356 | |||
6c3156273c | |||
eb7c804bb9 | |||
9d7bfae519 | |||
1292b5bf56 | |||
dbc5b5a3d1 | |||
1bd66a614b | |||
802a9cf83c | |||
9e95fdbb58 | |||
803f56cfe5 | |||
b163a6c5ad | |||
ddc119ceb6 | |||
09e621539e | |||
5e0b78b104 | |||
27852607d1 | |||
956719f797 | |||
71b8c59050 | |||
15c5fe4095 | |||
91ebcca0fd | |||
0826b40e2a | |||
911b62c63a | |||
5343c7af00 | |||
8c03c160a9 | |||
604b0467f9 | |||
a2b555dd81 | |||
9120367562 | |||
f509464947 | |||
07fd489982 | |||
f6d3831d6d | |||
d604ef2480 | |||
fe096772e0 | |||
35a63b8cc6 | |||
2a4596d415 | |||
35e476d916 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -63,6 +63,7 @@ yarn-error.log*
|
||||
|
||||
# Editor specific
|
||||
.vscode/*
|
||||
.idea/*
|
||||
|
||||
frontend-build
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
|
||||
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => {
|
||||
const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/secret-approvals`,
|
||||
@ -26,7 +27,7 @@ describe("Secret approval policy router", async () => {
|
||||
const policy = await createPolicy({
|
||||
secretPath: "/",
|
||||
approvals: 1,
|
||||
approvers: [seedData1.id],
|
||||
approvers: [{id:seedData1.id, type: ApproverType.User}],
|
||||
name: "test-policy"
|
||||
});
|
||||
|
||||
|
746
backend/package-lock.json
generated
746
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -131,6 +131,8 @@
|
||||
"@peculiar/x509": "^1.12.1",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@sindresorhus/slugify": "1.1.0",
|
||||
"@slack/oauth": "^3.0.1",
|
||||
"@slack/web-api": "^7.3.4",
|
||||
"@team-plain/typescript-sdk": "^4.6.1",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"ajv": "^8.12.0",
|
||||
@ -158,6 +160,7 @@
|
||||
"ldapjs": "^3.0.7",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"mongodb": "^6.8.1",
|
||||
"ms": "^2.1.3",
|
||||
"mysql2": "^3.9.8",
|
||||
"nanoid": "^3.3.4",
|
||||
@ -175,7 +178,7 @@
|
||||
"pino": "^8.16.2",
|
||||
"pkijs": "^3.2.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"probot": "^13.3.8",
|
||||
"safe-regex": "^2.1.1",
|
||||
"scim-patch": "^0.8.3",
|
||||
"scim2-parse-filter": "^0.2.10",
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@ -70,12 +70,14 @@ import { TSecretReplicationServiceFactory } from "@app/services/secret-replicati
|
||||
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
||||
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
||||
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
||||
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||
|
||||
declare module "fastify" {
|
||||
interface FastifyRequest {
|
||||
@ -177,6 +179,8 @@ declare module "fastify" {
|
||||
userEngagement: TUserEngagementServiceFactory;
|
||||
externalKms: TExternalKmsServiceFactory;
|
||||
orgAdmin: TOrgAdminServiceFactory;
|
||||
slack: TSlackServiceFactory;
|
||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
26
backend/src/@types/knex.d.ts
vendored
26
backend/src/@types/knex.d.ts
vendored
@ -193,6 +193,9 @@ import {
|
||||
TProjectRolesUpdate,
|
||||
TProjects,
|
||||
TProjectsInsert,
|
||||
TProjectSlackConfigs,
|
||||
TProjectSlackConfigsInsert,
|
||||
TProjectSlackConfigsUpdate,
|
||||
TProjectsUpdate,
|
||||
TProjectUserAdditionalPrivilege,
|
||||
TProjectUserAdditionalPrivilegeInsert,
|
||||
@ -299,6 +302,9 @@ import {
|
||||
TServiceTokens,
|
||||
TServiceTokensInsert,
|
||||
TServiceTokensUpdate,
|
||||
TSlackIntegrations,
|
||||
TSlackIntegrationsInsert,
|
||||
TSlackIntegrationsUpdate,
|
||||
TSuperAdmin,
|
||||
TSuperAdminInsert,
|
||||
TSuperAdminUpdate,
|
||||
@ -322,7 +328,10 @@ import {
|
||||
TUsersUpdate,
|
||||
TWebhooks,
|
||||
TWebhooksInsert,
|
||||
TWebhooksUpdate
|
||||
TWebhooksUpdate,
|
||||
TWorkflowIntegrations,
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TSecretV2TagJunction,
|
||||
@ -776,5 +785,20 @@ declare module "knex/types/tables" {
|
||||
TKmsKeyVersionsInsert,
|
||||
TKmsKeyVersionsUpdate
|
||||
>;
|
||||
[TableName.SlackIntegrations]: KnexOriginal.CompositeTableType<
|
||||
TSlackIntegrations,
|
||||
TSlackIntegrationsInsert,
|
||||
TSlackIntegrationsUpdate
|
||||
>;
|
||||
[TableName.ProjectSlackConfigs]: KnexOriginal.CompositeTableType<
|
||||
TProjectSlackConfigs,
|
||||
TProjectSlackConfigsInsert,
|
||||
TProjectSlackConfigsUpdate
|
||||
>;
|
||||
[TableName.WorkflowIntegrations]: KnexOriginal.CompositeTableType<
|
||||
TWorkflowIntegrations,
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,96 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.WorkflowIntegrations))) {
|
||||
await knex.schema.createTable(TableName.WorkflowIntegrations, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("integration").notNullable();
|
||||
tb.string("slug").notNullable();
|
||||
tb.uuid("orgId").notNullable();
|
||||
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
tb.string("description");
|
||||
tb.unique(["orgId", "slug"]);
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.WorkflowIntegrations);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) {
|
||||
await knex.schema.createTable(TableName.SlackIntegrations, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).notNullable();
|
||||
tb.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE");
|
||||
tb.string("teamId").notNullable();
|
||||
tb.string("teamName").notNullable();
|
||||
tb.string("slackUserId").notNullable();
|
||||
tb.string("slackAppId").notNullable();
|
||||
tb.binary("encryptedBotAccessToken").notNullable();
|
||||
tb.string("slackBotId").notNullable();
|
||||
tb.string("slackBotUserId").notNullable();
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectSlackConfigs))) {
|
||||
await knex.schema.createTable(TableName.ProjectSlackConfigs, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("projectId").notNullable().unique();
|
||||
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
tb.uuid("slackIntegrationId").notNullable();
|
||||
tb.foreign("slackIntegrationId").references("id").inTable(TableName.SlackIntegrations).onDelete("CASCADE");
|
||||
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||
tb.string("accessRequestChannels").notNullable().defaultTo("");
|
||||
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
|
||||
tb.string("secretRequestChannels").notNullable().defaultTo("");
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
|
||||
}
|
||||
|
||||
const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId");
|
||||
const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedSlackClientSecret"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
|
||||
if (!doesSuperAdminHaveSlackClientId) {
|
||||
tb.binary("encryptedSlackClientId");
|
||||
}
|
||||
if (!doesSuperAdminHaveSlackClientSecret) {
|
||||
tb.binary("encryptedSlackClientSecret");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SlackIntegrations);
|
||||
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.WorkflowIntegrations);
|
||||
await dropOnUpdateTrigger(knex, TableName.WorkflowIntegrations);
|
||||
|
||||
const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId");
|
||||
const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn(
|
||||
TableName.SuperAdmin,
|
||||
"encryptedSlackClientSecret"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
|
||||
if (doesSuperAdminHaveSlackClientId) {
|
||||
tb.dropColumn("encryptedSlackClientId");
|
||||
}
|
||||
if (doesSuperAdminHaveSlackClientSecret) {
|
||||
tb.dropColumn("encryptedSlackClientSecret");
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
|
||||
const hasRequireTemplateForIssuanceColumn = await knex.schema.hasColumn(
|
||||
TableName.CertificateAuthority,
|
||||
"requireTemplateForIssuance"
|
||||
);
|
||||
if (!hasRequireTemplateForIssuanceColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.dropColumn("requireTemplateForIssuance");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Certificate template
|
||||
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
|
||||
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateTemplate, (tb) => {
|
||||
if (!hasKeyUsagesCol) {
|
||||
tb.specificType("keyUsages", "text[]");
|
||||
}
|
||||
|
||||
if (!hasExtendedKeyUsagesCol) {
|
||||
tb.specificType("extendedKeyUsages", "text[]");
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasKeyUsagesCol) {
|
||||
await knex(TableName.CertificateTemplate).update({
|
||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasExtendedKeyUsagesCol) {
|
||||
await knex(TableName.CertificateTemplate).update({
|
||||
extendedKeyUsages: []
|
||||
});
|
||||
}
|
||||
|
||||
// Certificate
|
||||
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
|
||||
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
|
||||
await knex.schema.alterTable(TableName.Certificate, (tb) => {
|
||||
if (!doesCertTableHaveKeyUsages) {
|
||||
tb.specificType("keyUsages", "text[]");
|
||||
}
|
||||
|
||||
if (!doesCertTableHaveExtendedKeyUsages) {
|
||||
tb.specificType("extendedKeyUsages", "text[]");
|
||||
}
|
||||
});
|
||||
|
||||
if (!doesCertTableHaveKeyUsages) {
|
||||
await knex(TableName.Certificate).update({
|
||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
|
||||
});
|
||||
}
|
||||
|
||||
if (!doesCertTableHaveExtendedKeyUsages) {
|
||||
await knex(TableName.Certificate).update({
|
||||
extendedKeyUsages: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// Certificate Template
|
||||
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
|
||||
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateTemplate, (t) => {
|
||||
if (hasKeyUsagesCol) {
|
||||
t.dropColumn("keyUsages");
|
||||
}
|
||||
if (hasExtendedKeyUsagesCol) {
|
||||
t.dropColumn("extendedKeyUsages");
|
||||
}
|
||||
});
|
||||
|
||||
// Certificate
|
||||
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
|
||||
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
if (doesCertTableHaveKeyUsages) {
|
||||
t.dropColumn("keyUsages");
|
||||
}
|
||||
if (doesCertTableHaveExtendedKeyUsages) {
|
||||
t.dropColumn("extendedKeyUsages");
|
||||
}
|
||||
});
|
||||
}
|
@ -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.AccessApprovalPolicyApprover)) {
|
||||
// add column approverGroupId to AccessApprovalPolicyApprover
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||
// make nullable
|
||||
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
// make approverUserId nullable
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
});
|
||||
// add column approverGroupId to SecretApprovalPolicyApprover
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||
table.uuid("approverGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
table.uuid("approverUserId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||
// remove
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||
table.dropColumn("approverGroupId");
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
});
|
||||
|
||||
// remove
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||
table.dropColumn("approverGroupId");
|
||||
table.uuid("approverUserId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
@ -12,7 +12,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
approverUserId: z.string().uuid()
|
||||
approverUserId: z.string().uuid().nullable().optional(),
|
||||
approverGroupId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||
|
@ -28,7 +28,8 @@ export const CertificateAuthoritiesSchema = z.object({
|
||||
keyAlgorithm: z.string(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional(),
|
||||
activeCaCertId: z.string().uuid().nullable().optional()
|
||||
activeCaCertId: z.string().uuid().nullable().optional(),
|
||||
requireTemplateForIssuance: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||
|
@ -16,7 +16,9 @@ export const CertificateTemplatesSchema = z.object({
|
||||
subjectAlternativeName: z.string(),
|
||||
ttl: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
extendedKeyUsages: z.string().array().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
|
||||
|
@ -22,7 +22,9 @@ export const CertificatesSchema = z.object({
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().default("").nullable().optional(),
|
||||
caCertId: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional()
|
||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
extendedKeyUsages: z.string().array().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
|
@ -62,6 +62,7 @@ export * from "./project-environments";
|
||||
export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
export * from "./project-slack-configs";
|
||||
export * from "./project-user-additional-privilege";
|
||||
export * from "./project-user-membership-roles";
|
||||
export * from "./projects";
|
||||
@ -101,6 +102,7 @@ export * from "./secret-versions-v2";
|
||||
export * from "./secrets";
|
||||
export * from "./secrets-v2";
|
||||
export * from "./service-tokens";
|
||||
export * from "./slack-integrations";
|
||||
export * from "./super-admin";
|
||||
export * from "./trusted-ips";
|
||||
export * from "./user-actions";
|
||||
@ -109,3 +111,4 @@ export * from "./user-encryption-keys";
|
||||
export * from "./user-group-membership";
|
||||
export * from "./users";
|
||||
export * from "./webhooks";
|
||||
export * from "./workflow-integrations";
|
||||
|
@ -114,7 +114,10 @@ export enum TableName {
|
||||
InternalKms = "internal_kms",
|
||||
InternalKmsKeyVersion = "internal_kms_key_version",
|
||||
// @depreciated
|
||||
KmsKeyVersion = "kms_key_versions"
|
||||
KmsKeyVersion = "kms_key_versions",
|
||||
WorkflowIntegrations = "workflow_integrations",
|
||||
SlackIntegrations = "slack_integrations",
|
||||
ProjectSlackConfigs = "project_slack_configs"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
24
backend/src/db/schemas/project-slack-configs.ts
Normal file
24
backend/src/db/schemas/project-slack-configs.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectSlackConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
slackIntegrationId: z.string().uuid(),
|
||||
isAccessRequestNotificationEnabled: z.boolean().default(false),
|
||||
accessRequestChannels: z.string().default(""),
|
||||
isSecretRequestNotificationEnabled: z.boolean().default(false),
|
||||
secretRequestChannels: z.string().default(""),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectSlackConfigs = z.infer<typeof ProjectSlackConfigsSchema>;
|
||||
export type TProjectSlackConfigsInsert = Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>;
|
||||
export type TProjectSlackConfigsUpdate = Partial<Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>>;
|
@ -12,7 +12,8 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
approverUserId: z.string().uuid()
|
||||
approverUserId: z.string().uuid().nullable().optional(),
|
||||
approverGroupId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
||||
|
@ -21,8 +21,8 @@ export const SecretSharingSchema = z.object({
|
||||
expiresAfterViews: z.number().nullable().optional(),
|
||||
accessType: z.string().default("anyone"),
|
||||
name: z.string().nullable().optional(),
|
||||
password: z.string().nullable().optional(),
|
||||
lastViewedAt: z.date().nullable().optional()
|
||||
lastViewedAt: z.date().nullable().optional(),
|
||||
password: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
27
backend/src/db/schemas/slack-integrations.ts
Normal file
27
backend/src/db/schemas/slack-integrations.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SlackIntegrationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
teamId: z.string(),
|
||||
teamName: z.string(),
|
||||
slackUserId: z.string(),
|
||||
slackAppId: z.string(),
|
||||
encryptedBotAccessToken: zodBuffer,
|
||||
slackBotId: z.string(),
|
||||
slackBotUserId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSlackIntegrations = z.infer<typeof SlackIntegrationsSchema>;
|
||||
export type TSlackIntegrationsInsert = Omit<z.input<typeof SlackIntegrationsSchema>, TImmutableDBKeys>;
|
||||
export type TSlackIntegrationsUpdate = Partial<Omit<z.input<typeof SlackIntegrationsSchema>, TImmutableDBKeys>>;
|
@ -5,6 +5,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SuperAdminSchema = z.object({
|
||||
@ -19,7 +21,9 @@ export const SuperAdminSchema = z.object({
|
||||
trustLdapEmails: z.boolean().default(false).nullable().optional(),
|
||||
trustOidcEmails: z.boolean().default(false).nullable().optional(),
|
||||
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
||||
enabledLoginMethods: z.string().array().nullable().optional()
|
||||
enabledLoginMethods: z.string().array().nullable().optional(),
|
||||
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
22
backend/src/db/schemas/workflow-integrations.ts
Normal file
22
backend/src/db/schemas/workflow-integrations.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const WorkflowIntegrationsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
integration: z.string(),
|
||||
slug: z.string(),
|
||||
orgId: z.string().uuid(),
|
||||
description: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
|
||||
export type TWorkflowIntegrationsInsert = Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>;
|
||||
export type TWorkflowIntegrationsUpdate = Partial<Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>>;
|
@ -1,7 +1,9 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -10,28 +12,32 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
projectSlug: z.string().trim(),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
environment: z.string(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
environment: z.string(),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -50,6 +56,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
@ -58,14 +67,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
200: z.object({
|
||||
approvals: sapPubSchema
|
||||
.extend({
|
||||
userApprovers: z
|
||||
.object({
|
||||
userId: z.string()
|
||||
})
|
||||
.array(),
|
||||
secretPath: z.string().optional().nullable()
|
||||
approvers: z
|
||||
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -115,33 +125,37 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "PATCH",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).optional(),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
|
||||
policyId: req.params.policyId,
|
||||
@ -157,6 +171,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "DELETE",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
@ -167,7 +184,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -179,4 +196,44 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema.extend({
|
||||
approvers: z
|
||||
.object({
|
||||
type: z.nativeEnum(ApproverType),
|
||||
id: z.string().nullable().optional(),
|
||||
name: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.getAccessApprovalPolicyById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -11,6 +11,30 @@ export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get CRL in DER format (deprecated)",
|
||||
params: z.object({
|
||||
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
|
||||
}),
|
||||
response: {
|
||||
200: z.instanceof(Buffer)
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
|
||||
|
||||
res.header("Content-Type", "application/pkix-crl");
|
||||
|
||||
return Buffer.from(crl);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:crlId/der",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get CRL in DER format",
|
||||
params: z.object({
|
||||
|
@ -77,6 +77,39 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/entra-id/users",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
|
||||
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
|
||||
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string().min(1).describe("The name of the user"),
|
||||
id: z.string().min(1).describe("The ID of the user"),
|
||||
email: z.string().min(1).describe("The email of the user")
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
|
||||
tenantId: req.body.tenantId,
|
||||
applicationId: req.body.applicationId,
|
||||
clientSecret: req.body.clientSecret
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:name",
|
||||
@ -237,7 +270,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dynamicSecretCfgs = await server.services.dynamicSecret.list({
|
||||
const dynamicSecretCfgs = await server.services.dynamicSecret.listDynamicSecretsByEnv({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
|
@ -10,7 +10,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
|
||||
@ -43,12 +43,59 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:currentSlug",
|
||||
method: "PATCH",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
|
||||
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.getGroupById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
response: {
|
||||
200: GroupsSchema.array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const groups = await server.services.org.getOrgGroups({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id",
|
||||
method: "PATCH",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().describe(GROUPS.UPDATE.id)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
@ -70,7 +117,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.updateGroup({
|
||||
currentSlug: req.params.currentSlug,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -83,12 +130,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:slug",
|
||||
url: "/:id",
|
||||
method: "DELETE",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.DELETE.slug)
|
||||
id: z.string().trim().describe(GROUPS.DELETE.id)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
@ -96,7 +143,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const group = await server.services.group.deleteGroup({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -109,11 +156,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:slug/users",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id/users",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
|
||||
id: z.string().trim().describe(GROUPS.LIST_USERS.id)
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
||||
@ -141,24 +188,25 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { users, totalCount } = await server.services.group.listGroupUsers({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return { users, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:slug/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
|
||||
id: z.string().trim().describe(GROUPS.ADD_USER.id),
|
||||
username: z.string().trim().describe(GROUPS.ADD_USER.username)
|
||||
}),
|
||||
response: {
|
||||
@ -173,7 +221,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const user = await server.services.group.addUserToGroup({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
username: req.params.username,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -187,11 +235,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:slug/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/:id/users/:username",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
|
||||
id: z.string().trim().describe(GROUPS.DELETE_USER.id),
|
||||
username: z.string().trim().describe(GROUPS.DELETE_USER.username)
|
||||
}),
|
||||
response: {
|
||||
@ -206,7 +254,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const user = await server.services.group.removeUserFromGroup({
|
||||
groupSlug: req.params.slug,
|
||||
id: req.params.id,
|
||||
username: req.params.username,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
|
@ -101,6 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
}),
|
||||
response: {
|
||||
|
@ -87,6 +87,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Daniel: This endpoint is no longer is use.
|
||||
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
|
||||
*
|
||||
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
|
||||
*/
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs",
|
||||
@ -101,7 +107,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId)
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||
@ -122,6 +128,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
@ -138,16 +150,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogs = await server.services.auditLog.listProjectAuditLogs({
|
||||
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.query,
|
||||
endDate: req.query.endDate,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActor: req.query.actor,
|
||||
actor: req.permission.type
|
||||
actor: req.permission.type,
|
||||
|
||||
filter: {
|
||||
...req.query,
|
||||
projectId: req.params.workspaceId,
|
||||
endDate: req.query.endDate,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActorId: req.query.actor,
|
||||
eventType: req.query.eventType ? [req.query.eventType] : undefined
|
||||
}
|
||||
});
|
||||
return { auditLogs };
|
||||
}
|
||||
|
@ -100,17 +100,34 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
async (req, profile, cb) => {
|
||||
try {
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
||||
const email =
|
||||
profile?.email ??
|
||||
// entra sends data in this format
|
||||
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
|
||||
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
|
||||
|
||||
if (!email || !profile.firstName) {
|
||||
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
|
||||
const firstName = (profile.firstName ??
|
||||
// entra sends data in this format
|
||||
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
|
||||
|
||||
const lastName =
|
||||
profile.lastName ?? profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastName"];
|
||||
|
||||
if (!email || !firstName) {
|
||||
logger.info(
|
||||
{
|
||||
err: new Error("Invalid saml request. Missing email or first name"),
|
||||
profile
|
||||
},
|
||||
`email: ${email} firstName: ${profile.firstName as string}`
|
||||
);
|
||||
}
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||
externalId: profile.nameID,
|
||||
email,
|
||||
firstName: profile.firstName as string,
|
||||
lastName: profile.lastName as string,
|
||||
firstName,
|
||||
lastName: lastName as string,
|
||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -16,32 +17,33 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
workspaceId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.default("/")
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.default("/")
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -67,30 +69,31 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().optional(),
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -120,7 +123,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
@ -147,9 +150,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
200: z.object({
|
||||
approvals: sapPubSchema
|
||||
.extend({
|
||||
userApprovers: z
|
||||
approvers: z
|
||||
.object({
|
||||
userId: z.string()
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
@ -170,6 +174,44 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:sapId",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sapId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema.extend({
|
||||
approvers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType),
|
||||
name: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/board",
|
||||
method: "GET",
|
||||
@ -186,7 +228,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
200: z.object({
|
||||
policy: sapPubSchema
|
||||
.extend({
|
||||
userApprovers: z.object({ userId: z.string() }).array()
|
||||
userApprovers: z.object({ userId: z.string().nullable().optional() }).array()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const approvalRequestUser = z.object({ userId: z.string() }).merge(
|
||||
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
|
||||
UsersSchema.pick({
|
||||
email: true,
|
||||
firstName: true,
|
||||
@ -46,7 +46,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
approvers: z
|
||||
.object({
|
||||
userId: z.string().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string()
|
||||
}),
|
||||
@ -54,7 +58,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||
environment: z.string(),
|
||||
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
|
||||
approvers: z.string().array()
|
||||
approvers: z
|
||||
.object({
|
||||
userId: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
|
@ -5,22 +5,38 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApproverType } from "./access-approval-policy-types";
|
||||
|
||||
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
|
||||
|
||||
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
|
||||
const accessApprovalPolicyFindQuery = async (
|
||||
tx: Knex,
|
||||
filter: TFindFilter<TAccessApprovalPolicies>,
|
||||
customFilter?: {
|
||||
policyId?: string;
|
||||
}
|
||||
) => {
|
||||
const result = await tx(TableName.AccessApprovalPolicy)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.where((qb) => {
|
||||
if (customFilter?.policyId) {
|
||||
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
|
||||
}
|
||||
})
|
||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
|
||||
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
@ -30,10 +46,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
const findById = async (policyId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId
|
||||
});
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: doc,
|
||||
@ -50,9 +66,18 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId }) => ({
|
||||
userId: approverUserId
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId: id }) => ({
|
||||
id,
|
||||
type: "user"
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupId: id }) => ({
|
||||
id,
|
||||
type: "group"
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -64,9 +89,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
const find = async (
|
||||
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
||||
customFilter?: {
|
||||
policyId?: string;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
||||
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
|
||||
|
||||
const formattedDocs = sqlNestRelationships({
|
||||
data: docs,
|
||||
@ -84,9 +115,19 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId }) => ({
|
||||
userId: approverUserId
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId: id, approverUsername }) => ({
|
||||
id,
|
||||
type: ApproverType.User,
|
||||
name: approverUsername
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupId: id }) => ({
|
||||
id,
|
||||
type: ApproverType.Group
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -2,17 +2,21 @@ import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
||||
import {
|
||||
ApproverType,
|
||||
TCreateAccessApprovalPolicy,
|
||||
TDeleteAccessApprovalPolicy,
|
||||
TGetAccessApprovalPolicyByIdDTO,
|
||||
TGetAccessPolicyCountByEnvironmentDTO,
|
||||
TListAccessApprovalPoliciesDTO,
|
||||
TUpdateAccessApprovalPolicy
|
||||
@ -25,6 +29,8 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
groupDAL: TGroupDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||
@ -32,9 +38,11 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
|
||||
export const accessApprovalPolicyServiceFactory = ({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
groupDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
userDAL
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createAccessApprovalPolicy = async ({
|
||||
name,
|
||||
@ -52,7 +60,21 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (approvals > approvers.length)
|
||||
// If there is a group approver people might be added to the group later to meet the approvers quota
|
||||
const groupApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id) as string[];
|
||||
|
||||
const userApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -69,6 +91,47 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
let approverUserIds = userApprovers;
|
||||
if (userApproverNames.length) {
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
});
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
const usersPromises: Promise<
|
||||
{
|
||||
id: string;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
firstName: string | null | undefined;
|
||||
lastName: string | null | undefined;
|
||||
isPartOfGroup: boolean;
|
||||
}[]
|
||||
>[] = [];
|
||||
const verifyAllApprovers = [...approverUserIds];
|
||||
|
||||
for (const groupId of groupApprovers) {
|
||||
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||
}
|
||||
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||
.flat()
|
||||
.filter((user) => user.isPartOfGroup)
|
||||
.map((user) => user.id);
|
||||
verifyAllApprovers.push(...verifyGroupApprovers);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: project.id,
|
||||
orgId: actorOrgId,
|
||||
@ -76,7 +139,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: approvers
|
||||
userIds: verifyAllApprovers
|
||||
});
|
||||
|
||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
@ -90,13 +153,26 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((userId) => ({
|
||||
approverUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
if (approverUserIds.length) {
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
approverUserIds.map((userId) => ({
|
||||
approverUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupApprovers) {
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((groupId) => ({
|
||||
approverGroupId: groupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
return { ...accessApproval, environment: env, projectId: project.id };
|
||||
@ -138,7 +214,29 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
approvals,
|
||||
enforcementLevel
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const groupApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id) as string[];
|
||||
|
||||
const userApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
const currentAppovals = approvals || accessApprovalPolicy.approvals;
|
||||
if (
|
||||
groupApprovers?.length === 0 &&
|
||||
userApprovers &&
|
||||
currentAppovals > userApprovers.length + userApproverNames.length
|
||||
) {
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
}
|
||||
|
||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@ -161,7 +259,30 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (approvers) {
|
||||
|
||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
|
||||
if (userApprovers.length || userApproverNames.length) {
|
||||
let userApproverIds = userApprovers;
|
||||
if (userApproverNames.length) {
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
});
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
@ -169,18 +290,56 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: approvers
|
||||
userIds: userApproverIds
|
||||
});
|
||||
|
||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((userId) => ({
|
||||
userApproverIds.map((userId) => ({
|
||||
approverUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupApprovers) {
|
||||
const usersPromises: Promise<
|
||||
{
|
||||
id: string;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
firstName: string | null | undefined;
|
||||
lastName: string | null | undefined;
|
||||
isPartOfGroup: boolean;
|
||||
}[]
|
||||
>[] = [];
|
||||
|
||||
for (const groupId of groupApprovers) {
|
||||
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||
}
|
||||
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||
.flat()
|
||||
.filter((user) => user.isPartOfGroup)
|
||||
.map((user) => user.id);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: verifyGroupApprovers
|
||||
});
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((groupId) => ({
|
||||
approverGroupId: groupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
@ -246,11 +405,40 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
return { count: policies.length };
|
||||
};
|
||||
|
||||
const getAccessApprovalPolicyById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
policyId
|
||||
}: TGetAccessApprovalPolicyByIdDTO) => {
|
||||
const [policy] = await accessApprovalPolicyDAL.find({}, { policyId });
|
||||
|
||||
if (!policy) {
|
||||
throw new NotFoundError({
|
||||
message: "Cannot find access approval policy"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
policy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
return policy;
|
||||
};
|
||||
|
||||
return {
|
||||
getAccessPolicyCountByEnvSlug,
|
||||
createAccessApprovalPolicy,
|
||||
deleteAccessApprovalPolicy,
|
||||
updateAccessApprovalPolicy,
|
||||
getAccessApprovalPolicyByProjectSlug
|
||||
getAccessApprovalPolicyByProjectSlug,
|
||||
getAccessApprovalPolicyById
|
||||
};
|
||||
};
|
||||
|
@ -13,11 +13,16 @@ export type TVerifyApprovers = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export enum ApproverType {
|
||||
Group = "group",
|
||||
User = "user"
|
||||
}
|
||||
|
||||
export type TCreateAccessApprovalPolicy = {
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
@ -26,7 +31,7 @@ export type TCreateAccessApprovalPolicy = {
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
approvals?: number;
|
||||
approvers?: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
@ -41,6 +46,10 @@ export type TGetAccessPolicyCountByEnvironmentDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetAccessApprovalPolicyByIdDTO = {
|
||||
policyId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListAccessApprovalPoliciesDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -39,6 +39,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("requestedByUser"),
|
||||
@ -59,6 +65,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
|
||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
|
||||
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
@ -142,7 +149,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
||||
},
|
||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
|
||||
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupUserId }) => approverGroupUserId
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@ -172,17 +184,28 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`requestedByUser.id`
|
||||
)
|
||||
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.join<TUsers>(
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||
"accessApprovalPolicyApproverUser.id"
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
|
||||
`${TableName.UserGroupMembership}.userId`,
|
||||
"accessApprovalPolicyGroupApproverUser.id"
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
@ -200,10 +223,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
|
||||
tx.ref("userId").withSchema(TableName.UserGroupMembership),
|
||||
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
|
||||
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
|
||||
tx.ref("username").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
|
||||
tx.ref("firstName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
|
||||
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
|
||||
tx.ref("lastName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
|
||||
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
||||
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
||||
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
||||
@ -282,6 +310,23 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "userId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({
|
||||
userId,
|
||||
approverGroupEmail: email,
|
||||
approverGroupUsername: username,
|
||||
approverGroupLastName: lastName,
|
||||
approverFirstName: firstName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -5,15 +5,20 @@ import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
@ -33,7 +38,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
|
||||
projectDAL: Pick<
|
||||
TProjectDALFactory,
|
||||
"checkProjectUpgradeStatus" | "findProjectBySlug" | "findProjectWithOrg" | "findById"
|
||||
>;
|
||||
accessApprovalRequestDAL: Pick<
|
||||
TAccessApprovalRequestDALFactory,
|
||||
| "create"
|
||||
@ -50,17 +58,21 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
>;
|
||||
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "find" | "findById"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
|
||||
export const accessApprovalRequestServiceFactory = ({
|
||||
groupDAL,
|
||||
projectDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
@ -71,7 +83,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
accessApprovalPolicyApproverDAL,
|
||||
additionalPrivilegeDAL,
|
||||
smtpService,
|
||||
userDAL
|
||||
userDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const createAccessApprovalRequest = async ({
|
||||
isTemporary,
|
||||
@ -113,13 +127,36 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
});
|
||||
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
|
||||
|
||||
const approverIds: string[] = [];
|
||||
const approverGroupIds: string[] = [];
|
||||
|
||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||
policyId: policy.id
|
||||
});
|
||||
|
||||
approvers.forEach((approver) => {
|
||||
if (approver.approverUserId) {
|
||||
approverIds.push(approver.approverUserId);
|
||||
} else if (approver.approverGroupId) {
|
||||
approverGroupIds.push(approver.approverGroupId);
|
||||
}
|
||||
});
|
||||
|
||||
const groupUsers = (
|
||||
await Promise.all(
|
||||
approverGroupIds.map((groupApproverId) =>
|
||||
groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: actorOrgId,
|
||||
groupId: groupApproverId
|
||||
})
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
approverIds.push(...groupUsers.filter((user) => user.isPartOfGroup).map((user) => user.id));
|
||||
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
id: approvers.map((approver) => approver.approverUserId)
|
||||
id: [...new Set(approverIds)]
|
||||
}
|
||||
});
|
||||
|
||||
@ -166,13 +203,36 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
|
||||
const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`;
|
||||
|
||||
await triggerSlackNotification({
|
||||
projectId: project.id,
|
||||
projectSlackConfigDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.ACCESS_REQUEST,
|
||||
payload: {
|
||||
projectName: project.name,
|
||||
requesterFullName,
|
||||
isTemporary,
|
||||
requesterEmail: requestedByUser.email as string,
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request",
|
||||
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
|
||||
requesterFullName,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary,
|
||||
...(isTemporary && {
|
||||
@ -181,7 +241,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
||||
approvalUrl
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequest
|
||||
});
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { AuditLogsSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { EventType } from "./audit-log-types";
|
||||
|
||||
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
|
||||
|
||||
@ -25,31 +28,104 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||
|
||||
const find = async (
|
||||
{ orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery,
|
||||
{
|
||||
orgId,
|
||||
projectId,
|
||||
userAgentType,
|
||||
startDate,
|
||||
endDate,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
actorId,
|
||||
actorType,
|
||||
eventType,
|
||||
eventMetadata
|
||||
}: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||
actorId?: string;
|
||||
actorType?: ActorType;
|
||||
eventType?: EventType[];
|
||||
eventMetadata?: Record<string, string>;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
if (!orgId && !projectId) {
|
||||
throw new Error("Either orgId or projectId must be provided");
|
||||
}
|
||||
|
||||
try {
|
||||
// Find statements
|
||||
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
orgId,
|
||||
eventType,
|
||||
actor,
|
||||
userAgentType
|
||||
})
|
||||
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
||||
// eslint-disable-next-line func-names
|
||||
.where(function () {
|
||||
if (orgId) {
|
||||
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId);
|
||||
} else if (projectId) {
|
||||
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
||||
}
|
||||
});
|
||||
|
||||
if (userAgentType) {
|
||||
void sqlQuery.where("userAgentType", userAgentType);
|
||||
}
|
||||
|
||||
// Select statements
|
||||
void sqlQuery
|
||||
.select(selectAllTableCols(TableName.AuditLog))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.orderBy("createdAt", "desc");
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||
|
||||
// Special case: Filter by actor ID
|
||||
if (actorId) {
|
||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
|
||||
}
|
||||
|
||||
// Special case: Filter by key/value pairs in eventMetadata field
|
||||
if (eventMetadata && Object.keys(eventMetadata).length) {
|
||||
Object.entries(eventMetadata).forEach(([key, value]) => {
|
||||
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by actor type
|
||||
if (actorType) {
|
||||
void sqlQuery.where("actor", actorType);
|
||||
}
|
||||
|
||||
// Filter by event types
|
||||
if (eventType?.length) {
|
||||
void sqlQuery.whereIn("eventType", eventType);
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (startDate) {
|
||||
void sqlQuery.where("createdAt", ">=", startDate);
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
void sqlQuery.where("createdAt", "<=", endDate);
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
|
||||
}
|
||||
const docs = await sqlQuery;
|
||||
return docs;
|
||||
|
||||
return docs.map((doc) => {
|
||||
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
|
||||
// This is a quick and dirty way to get around the types.
|
||||
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
|
||||
|
||||
return {
|
||||
...AuditLogsSchema.parse(doc),
|
||||
...(projectDoc?.projectSlug && {
|
||||
project: {
|
||||
name: projectDoc.projectName,
|
||||
slug: projectDoc.projectSlug
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error });
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||
@ -11,7 +12,7 @@ import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-
|
||||
|
||||
type TAuditLogServiceFactoryDep = {
|
||||
auditLogDAL: TAuditLogDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
auditLogQueue: TAuditLogQueueServiceFactory;
|
||||
};
|
||||
|
||||
@ -22,38 +23,48 @@ export const auditLogServiceFactory = ({
|
||||
auditLogQueue,
|
||||
permissionService
|
||||
}: TAuditLogServiceFactoryDep) => {
|
||||
const listProjectAuditLogs = async ({
|
||||
userAgentType,
|
||||
eventType,
|
||||
offset,
|
||||
limit,
|
||||
endDate,
|
||||
startDate,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
auditLogActor
|
||||
}: TListProjectAuditLogDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
||||
// Filter logs for specific project
|
||||
if (filter.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
filter.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
} else {
|
||||
// Organization-wide logs
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
/**
|
||||
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
||||
* to the organization level ✅
|
||||
*/
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
}
|
||||
|
||||
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||
const auditLogs = await auditLogDAL.find({
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
offset,
|
||||
eventType,
|
||||
userAgentType,
|
||||
actor: auditLogActor,
|
||||
projectId
|
||||
startDate: filter.startDate,
|
||||
endDate: filter.endDate,
|
||||
limit: filter.limit,
|
||||
offset: filter.offset,
|
||||
eventType: filter.eventType,
|
||||
userAgentType: filter.userAgentType,
|
||||
actorId: filter.auditLogActorId,
|
||||
actorType: filter.actorType,
|
||||
eventMetadata: filter.eventMetadata,
|
||||
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
||||
});
|
||||
|
||||
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
||||
...el,
|
||||
event: { type: logEventType, metadata: eventMetadata },
|
||||
@ -76,6 +87,6 @@ export const auditLogServiceFactory = ({
|
||||
|
||||
return {
|
||||
createAuditLog,
|
||||
listProjectAuditLogs
|
||||
listAuditLogs
|
||||
};
|
||||
};
|
||||
|
@ -5,19 +5,23 @@ import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
auditLogActor?: string;
|
||||
projectId: string;
|
||||
eventType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
userAgentType?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} & TProjectPermission;
|
||||
filter: {
|
||||
userAgentType?: UserAgentType;
|
||||
eventType?: EventType[];
|
||||
offset?: number;
|
||||
limit: number;
|
||||
endDate?: string;
|
||||
startDate?: string;
|
||||
projectId?: string;
|
||||
auditLogActorId?: string;
|
||||
actorType?: ActorType;
|
||||
eventMetadata?: Record<string, string>;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAuditLogDTO = {
|
||||
event: Event;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
|
||||
orgId?: string;
|
||||
projectId?: string;
|
||||
} & BaseAuthData;
|
||||
@ -140,6 +144,7 @@ export enum EventType {
|
||||
GET_CA_CRLS = "get-certificate-authority-crls",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
SIGN_CERT = "sign-cert",
|
||||
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
@ -169,7 +174,15 @@ export enum EventType {
|
||||
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"
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||
GET_SLACK_INTEGRATION = "get-slack-integration",
|
||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
INTEGRATION_SYNCED = "integration-synced"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -190,6 +203,8 @@ interface IdentityActorMetadata {
|
||||
|
||||
interface ScimClientActorMetadata {}
|
||||
|
||||
interface PlatformActorMetadata {}
|
||||
|
||||
export interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
@ -200,6 +215,11 @@ export interface ServiceActor {
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export interface PlatformActor {
|
||||
type: ActorType.PLATFORM;
|
||||
metadata: PlatformActorMetadata;
|
||||
}
|
||||
|
||||
export interface IdentityActor {
|
||||
type: ActorType.IDENTITY;
|
||||
metadata: IdentityActorMetadata;
|
||||
@ -210,7 +230,7 @@ export interface ScimClientActor {
|
||||
metadata: ScimClientActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
@ -1192,6 +1212,14 @@ interface SignCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCertificateTemplates {
|
||||
type: EventType.GET_CA_CERTIFICATE_TEMPLATES;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCert {
|
||||
type: EventType.GET_CERT;
|
||||
metadata: {
|
||||
@ -1446,6 +1474,73 @@ interface GetCertificateTemplateEstConfig {
|
||||
};
|
||||
}
|
||||
|
||||
interface AttemptCreateSlackIntegration {
|
||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AttemptReinstallSlackIntegration {
|
||||
type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSlackIntegration {
|
||||
type: EventType.UPDATE_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
id: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSlackIntegration {
|
||||
type: EventType.DELETE_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSlackIntegration {
|
||||
type: EventType.GET_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectSlackConfig {
|
||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
slackIntegrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectSlackConfig {
|
||||
type: EventType.GET_PROJECT_SLACK_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
interface IntegrationSyncedEvent {
|
||||
type: EventType.INTEGRATION_SYNCED;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
lastSyncJobId: string;
|
||||
lastUsed: Date;
|
||||
syncMessage: string;
|
||||
isSynced: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1547,6 +1642,7 @@ export type Event =
|
||||
| GetCaCrls
|
||||
| IssueCert
|
||||
| SignCert
|
||||
| GetCaCertificateTemplates
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
@ -1576,4 +1672,12 @@ export type Event =
|
||||
| DeleteCertificateTemplate
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig;
|
||||
| GetCertificateTemplateEstConfig
|
||||
| AttemptCreateSlackIntegration
|
||||
| AttemptReinstallSlackIntegration
|
||||
| UpdateSlackIntegration
|
||||
| DeleteSlackIntegration
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| IntegrationSyncedEvent;
|
||||
|
@ -1,10 +1,70 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
|
||||
|
||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.DynamicSecret);
|
||||
return orm;
|
||||
|
||||
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||
const listDynamicSecretsByFolderIds = async (
|
||||
{
|
||||
folderIds,
|
||||
search,
|
||||
limit,
|
||||
offset = 0,
|
||||
orderBy = SecretsOrderBy.Name,
|
||||
orderDirection = OrderByDirection.ASC
|
||||
}: {
|
||||
folderIds: string[];
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||
.whereIn("folderId", folderIds)
|
||||
.where((bd) => {
|
||||
if (search) {
|
||||
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
||||
}
|
||||
})
|
||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.DynamicSecret),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
|
||||
)
|
||||
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||
|
||||
if (limit) {
|
||||
const rankOffset = offset + 1;
|
||||
return await (tx || db)
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", rankOffset)
|
||||
.andWhere("w.rank", "<", rankOffset + limit);
|
||||
}
|
||||
|
||||
const dynamicSecrets = await query;
|
||||
|
||||
return dynamicSecrets;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "List dynamic secret multi env" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, listDynamicSecretsByFolderIds };
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
@ -17,9 +18,12 @@ import {
|
||||
TCreateDynamicSecretDTO,
|
||||
TDeleteDynamicSecretDTO,
|
||||
TDetailsDynamicSecretDTO,
|
||||
TGetDynamicSecretsCountDTO,
|
||||
TListDynamicSecretsDTO,
|
||||
TListDynamicSecretsMultiEnvDTO,
|
||||
TUpdateDynamicSecretDTO
|
||||
} from "./dynamic-secret-types";
|
||||
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||
|
||||
type TDynamicSecretServiceFactoryDep = {
|
||||
@ -31,7 +35,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
||||
>;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@ -300,19 +304,58 @@ export const dynamicSecretServiceFactory = ({
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
};
|
||||
|
||||
const list = async ({
|
||||
// get unique dynamic secret count across multiple envs
|
||||
const getCountMultiEnv = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
projectSlug,
|
||||
projectId,
|
||||
path,
|
||||
environmentSlug
|
||||
}: TListDynamicSecretsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
environmentSlugs,
|
||||
search,
|
||||
isInternal
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const projectId = project.id;
|
||||
// verify user has access to each env in request
|
||||
environmentSlugs.forEach((environmentSlug) =>
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||
{ $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined },
|
||||
{ countDistinct: "name" }
|
||||
);
|
||||
|
||||
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
// get dynamic secret count for a single env
|
||||
const getDynamicSecretCount = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
path,
|
||||
environmentSlug,
|
||||
search,
|
||||
projectId
|
||||
}: TGetDynamicSecretsCountDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -328,15 +371,130 @@ export const dynamicSecretServiceFactory = ({
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||
{ count: true }
|
||||
);
|
||||
return Number(dynamicSecretCfg[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
const listDynamicSecretsByEnv = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
projectSlug,
|
||||
path,
|
||||
environmentSlug,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
search,
|
||||
...params
|
||||
}: TListDynamicSecretsDTO) => {
|
||||
let { projectId } = params;
|
||||
|
||||
if (!projectId) {
|
||||
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
projectId = project.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
||||
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||
{
|
||||
limit,
|
||||
offset,
|
||||
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
||||
}
|
||||
);
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
// get dynamic secrets for multiple envs
|
||||
const listDynamicSecretsByFolderIds = async ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actor,
|
||||
path,
|
||||
environmentSlugs,
|
||||
projectId,
|
||||
isInternal,
|
||||
...params
|
||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||
if (!isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
// verify user has access to each env in request
|
||||
environmentSlugs.forEach((environmentSlug) =>
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
|
||||
folderIds: folders.map((folder) => folder.id),
|
||||
...params
|
||||
});
|
||||
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async ({
|
||||
tenantId,
|
||||
applicationId,
|
||||
clientSecret
|
||||
}: {
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
clientSecret: string;
|
||||
}) => {
|
||||
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
|
||||
tenantId,
|
||||
applicationId,
|
||||
clientSecret
|
||||
);
|
||||
return azureEntraIdUsers;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateByName,
|
||||
deleteByName,
|
||||
getDetails,
|
||||
list
|
||||
listDynamicSecretsByEnv,
|
||||
listDynamicSecretsByFolderIds,
|
||||
getDynamicSecretCount,
|
||||
getCountMultiEnv,
|
||||
fetchAzureEntraIdUsers
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||
|
||||
@ -50,5 +51,20 @@ export type TDetailsDynamicSecretDTO = {
|
||||
export type TListDynamicSecretsDTO = {
|
||||
path: string;
|
||||
environmentSlug: string;
|
||||
projectSlug: string;
|
||||
projectSlug?: string;
|
||||
projectId?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListDynamicSecretsMultiEnvDTO = Omit<
|
||||
TListDynamicSecretsDTO,
|
||||
"projectId" | "environmentSlug" | "projectSlug"
|
||||
> & { projectId: string; environmentSlugs: string[]; isInternal?: boolean };
|
||||
|
||||
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
|
||||
projectId: string;
|
||||
};
|
||||
|
@ -0,0 +1,138 @@
|
||||
import axios from "axios";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
|
||||
const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
type User = { name: string; id: string; email: string };
|
||||
|
||||
export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
fetchAzureEntraIdUsers: (tenantId: string, applicationId: string, clientSecret: string) => Promise<User[]>;
|
||||
} => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await AzureEntraIDSchema.parseAsync(inputs);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getToken = async (
|
||||
tenantId: string,
|
||||
applicationId: string,
|
||||
clientSecret: string
|
||||
): Promise<{ token?: string; success: boolean }> => {
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
`${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`,
|
||||
{
|
||||
grant_type: "client_credentials",
|
||||
client_id: applicationId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default"
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
return { token: response.data.access_token, success: true };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
// Do nothing
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
const password = generatePassword();
|
||||
|
||||
const response = await axios.patch(
|
||||
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||
{
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (response.status !== 204) {
|
||||
throw new BadRequestError({ message: "Failed to update password" });
|
||||
}
|
||||
|
||||
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
// Creates a new password
|
||||
await create(inputs);
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||
const data = await getToken(tenantId, applicationId, clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>(
|
||||
`${MSFT_GRAPH_API_URL}/users`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new BadRequestError({ message: "Failed to fetch users" });
|
||||
}
|
||||
|
||||
const users = response.data.value.map((user) => {
|
||||
return {
|
||||
name: user.displayName,
|
||||
id: user.id,
|
||||
email: user.userPrincipalName
|
||||
};
|
||||
});
|
||||
return users;
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew,
|
||||
fetchAzureEntraIdUsers
|
||||
};
|
||||
};
|
@ -17,7 +17,7 @@ const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => {
|
||||
export const ElasticSearchProvider = (): 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
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { ElasticSearchDatabaseProvider } from "./elastic-search";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||
import { MongoDBProvider } from "./mongo-db";
|
||||
import { RabbitMqProvider } from "./rabbit-mq";
|
||||
import { RedisDatabaseProvider } from "./redis";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
|
||||
@ -14,5 +17,8 @@ export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
|
||||
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
|
||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider()
|
||||
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider()
|
||||
});
|
||||
|
@ -17,7 +17,6 @@ export const DynamicSecretRedisDBSchema = z.object({
|
||||
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(),
|
||||
@ -57,6 +56,26 @@ export const DynamicSecretElasticSearchSchema = z.object({
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretRabbitMqSchema = z.object({
|
||||
host: z.string().trim().min(1),
|
||||
port: z.number(),
|
||||
tags: z.array(z.string().trim()).default([]),
|
||||
|
||||
username: z.string().trim().min(1),
|
||||
password: z.string().trim().min(1),
|
||||
|
||||
ca: z.string().optional(),
|
||||
|
||||
virtualHost: z.object({
|
||||
name: z.string().trim().min(1),
|
||||
permissions: z.object({
|
||||
read: z.string().trim().min(1),
|
||||
write: z.string().trim().min(1),
|
||||
configure: z.string().trim().min(1)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
export const DynamicSecretSqlDBSchema = z.object({
|
||||
client: z.nativeEnum(SqlProviders),
|
||||
host: z.string().trim().toLowerCase(),
|
||||
@ -131,6 +150,30 @@ export const DynamicSecretMongoAtlasSchema = z.object({
|
||||
.array()
|
||||
});
|
||||
|
||||
export const DynamicSecretMongoDBSchema = z.object({
|
||||
host: z.string().min(1).trim().toLowerCase(),
|
||||
port: z.number().optional(),
|
||||
username: z.string().min(1).trim(),
|
||||
password: z.string().min(1).trim(),
|
||||
database: z.string().min(1).trim(),
|
||||
ca: z.string().min(1).optional(),
|
||||
roles: z
|
||||
.string()
|
||||
.array()
|
||||
.min(1)
|
||||
.describe(
|
||||
'Enum: "atlasAdmin" "backup" "clusterMonitor" "dbAdmin" "dbAdminAnyDatabase" "enableSharding" "read" "readAnyDatabase" "readWrite" "readWriteAnyDatabase" "<a custom role name>".Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.'
|
||||
)
|
||||
});
|
||||
|
||||
export const AzureEntraIDSchema = z.object({
|
||||
tenantId: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
email: z.string().trim().min(1),
|
||||
applicationId: z.string().trim().min(1),
|
||||
clientSecret: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
@ -138,7 +181,10 @@ export enum DynamicSecretProviders {
|
||||
Redis = "redis",
|
||||
AwsElastiCache = "aws-elasticache",
|
||||
MongoAtlas = "mongo-db-atlas",
|
||||
ElasticSearch = "elastic-search"
|
||||
ElasticSearch = "elastic-search",
|
||||
MongoDB = "mongo-db",
|
||||
RabbitMq = "rabbit-mq",
|
||||
AzureEntraID = "azure-entra-id"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@ -148,7 +194,10 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
116
backend/src/ee/services/dynamic-secret/providers/mongo-db.ts
Normal file
116
backend/src/ee/services/dynamic-secret/providers/mongo-db.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
||||
if (
|
||||
appCfg.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") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||
const isSrv = !providerInputs.port;
|
||||
const uri = isSrv
|
||||
? `mongodb+srv://${providerInputs.host}`
|
||||
: `mongodb://${providerInputs.host}:${providerInputs.port}`;
|
||||
|
||||
const client = new MongoClient(uri, {
|
||||
auth: {
|
||||
username: providerInputs.username,
|
||||
password: providerInputs.password
|
||||
},
|
||||
directConnection: !isSrv,
|
||||
ca: providerInputs.ca
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const isConnected = await client
|
||||
.db(providerInputs.database)
|
||||
.command({ ping: 1 })
|
||||
.then(() => true);
|
||||
|
||||
await client.close();
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
|
||||
await db.command({
|
||||
createUser: username,
|
||||
pwd: password,
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
await client.close();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
await db.command({
|
||||
dropUser: username
|
||||
});
|
||||
await client.close();
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
172
backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts
Normal file
172
backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import axios, { Axios } from "axios";
|
||||
import https from "https";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
type TCreateRabbitMQUser = {
|
||||
axiosInstance: Axios;
|
||||
createUser: {
|
||||
username: string;
|
||||
password: string;
|
||||
tags: string[];
|
||||
};
|
||||
virtualHost: {
|
||||
name: string;
|
||||
permissions: {
|
||||
read: string;
|
||||
write: string;
|
||||
configure: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type TDeleteRabbitMqUser = {
|
||||
axiosInstance: Axios;
|
||||
usernameToDelete: string;
|
||||
};
|
||||
|
||||
async function createRabbitMqUser({ axiosInstance, createUser, virtualHost }: TCreateRabbitMQUser): Promise<void> {
|
||||
try {
|
||||
// Create user
|
||||
const userUrl = `/users/${createUser.username}`;
|
||||
const userData = {
|
||||
password: createUser.password,
|
||||
tags: createUser.tags.join(",")
|
||||
};
|
||||
|
||||
await axiosInstance.put(userUrl, userData);
|
||||
|
||||
// Set permissions for the virtual host
|
||||
if (virtualHost) {
|
||||
const permissionData = {
|
||||
configure: virtualHost.permissions.configure,
|
||||
write: virtualHost.permissions.write,
|
||||
read: virtualHost.permissions.read
|
||||
};
|
||||
|
||||
await axiosInstance.put(
|
||||
`/permissions/${encodeURIComponent(virtualHost.name)}/${createUser.username}`,
|
||||
permissionData
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, "Error creating RabbitMQ user");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRabbitMqUser) {
|
||||
await axiosInstance.delete(`users/${usernameToDelete}`);
|
||||
return { username: usernameToDelete };
|
||||
}
|
||||
|
||||
export const RabbitMqProvider = (): 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 providerInputs = await DynamicSecretRabbitMqSchema.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") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
|
||||
auth: {
|
||||
username: providerInputs.username,
|
||||
password: providerInputs.password
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
|
||||
...(providerInputs.ca && {
|
||||
httpsAgent: new https.Agent({ ca: providerInputs.ca, rejectUnauthorized: false })
|
||||
})
|
||||
});
|
||||
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
|
||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||
|
||||
return infoResponse;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
|
||||
await createRabbitMqUser({
|
||||
axiosInstance: connection,
|
||||
virtualHost: providerInputs.virtualHost,
|
||||
createUser: {
|
||||
password,
|
||||
username,
|
||||
tags: [...(providerInputs.tags ?? []), "infisical-user"]
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
// Do nothing
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -60,7 +60,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllGroupMembers = async ({
|
||||
const findAllGroupPossibleMembers = async ({
|
||||
orgId,
|
||||
groupId,
|
||||
offset = 0,
|
||||
@ -125,7 +125,7 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
findAllGroupMembers,
|
||||
findAllGroupPossibleMembers,
|
||||
...groupOrm
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@ -21,6 +21,7 @@ import {
|
||||
TAddUserToGroupDTO,
|
||||
TCreateGroupDTO,
|
||||
TDeleteGroupDTO,
|
||||
TGetGroupByIdDTO,
|
||||
TListGroupUsersDTO,
|
||||
TRemoveUserFromGroupDTO,
|
||||
TUpdateGroupDTO
|
||||
@ -29,7 +30,10 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||
|
||||
type TGroupServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
@ -95,7 +99,7 @@ export const groupServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateGroup = async ({
|
||||
currentSlug,
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
role,
|
||||
@ -121,8 +125,10 @@ export const groupServiceFactory = ({
|
||||
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
|
||||
});
|
||||
|
||||
const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
|
||||
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
|
||||
const group = await groupDAL.findOne({ orgId: actorOrgId, id });
|
||||
if (!group) {
|
||||
throw new BadRequestError({ message: `Failed to find group with ID ${id}` });
|
||||
}
|
||||
|
||||
let customRole: TOrgRoles | undefined;
|
||||
if (role) {
|
||||
@ -140,8 +146,7 @@ export const groupServiceFactory = ({
|
||||
|
||||
const [updatedGroup] = await groupDAL.update(
|
||||
{
|
||||
orgId: actorOrgId,
|
||||
slug: currentSlug
|
||||
id: group.id
|
||||
},
|
||||
{
|
||||
name,
|
||||
@ -158,7 +163,7 @@ export const groupServiceFactory = ({
|
||||
return updatedGroup;
|
||||
};
|
||||
|
||||
const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
@ -178,15 +183,39 @@ export const groupServiceFactory = ({
|
||||
});
|
||||
|
||||
const [group] = await groupDAL.delete({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
|
||||
if (!actorOrgId) {
|
||||
throw new BadRequestError({ message: "Failed to read group without organization" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findById(id);
|
||||
if (!group) {
|
||||
throw new NotFoundError({
|
||||
message: `Cannot find group with ID ${id}`
|
||||
});
|
||||
}
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
const listGroupUsers = async ({
|
||||
groupSlug,
|
||||
id,
|
||||
offset,
|
||||
limit,
|
||||
username,
|
||||
@ -208,15 +237,15 @@ export const groupServiceFactory = ({
|
||||
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const users = await groupDAL.findAllGroupMembers({
|
||||
const users = await groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id,
|
||||
offset,
|
||||
@ -229,14 +258,7 @@ export const groupServiceFactory = ({
|
||||
return { users, totalCount: count };
|
||||
};
|
||||
|
||||
const addUserToGroup = async ({
|
||||
groupSlug,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TAddUserToGroupDTO) => {
|
||||
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
@ -251,12 +273,12 @@ export const groupServiceFactory = ({
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
@ -285,7 +307,7 @@ export const groupServiceFactory = ({
|
||||
};
|
||||
|
||||
const removeUserFromGroup = async ({
|
||||
groupSlug,
|
||||
id,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
@ -306,12 +328,12 @@ export const groupServiceFactory = ({
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
slug: groupSlug
|
||||
id
|
||||
});
|
||||
|
||||
if (!group)
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find group with slug ${groupSlug}`
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
@ -342,6 +364,7 @@ export const groupServiceFactory = ({
|
||||
deleteGroup,
|
||||
listGroupUsers,
|
||||
addUserToGroup,
|
||||
removeUserFromGroup
|
||||
removeUserFromGroup,
|
||||
getGroupById
|
||||
};
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ export type TCreateGroupDTO = {
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TUpdateGroupDTO = {
|
||||
currentSlug: string;
|
||||
id: string;
|
||||
} & Partial<{
|
||||
name: string;
|
||||
slug: string;
|
||||
@ -26,23 +26,27 @@ export type TUpdateGroupDTO = {
|
||||
TGenericPermission;
|
||||
|
||||
export type TDeleteGroupDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TGetGroupByIdDTO = {
|
||||
id: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TListGroupUsersDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
username?: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TAddUserToGroupDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
username: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TRemoveUserFromGroupDTO = {
|
||||
groupSlug: string;
|
||||
id: string;
|
||||
username: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
|
@ -41,10 +41,9 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// special query
|
||||
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
|
||||
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const usernameDocs: string[] = await db
|
||||
.replicaNode()(TableName.UserGroupMembership)
|
||||
const usernameDocs: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership)
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
||||
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
|
||||
export enum OrgPermissionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@ -27,7 +25,8 @@ export enum OrgPermissionSubjects {
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
AdminConsole = "organization-admin-console",
|
||||
AuditLogs = "audit-logs"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@ -45,10 +44,11 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
// ws permissions
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
@ -113,15 +113,20 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
return rules;
|
||||
};
|
||||
|
||||
export const orgAdminPermissions = buildAdminPermission();
|
||||
|
||||
const buildMemberPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
@ -142,14 +147,16 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
export const orgMemberPermissions = buildMemberPermission();
|
||||
|
||||
const buildNoAccessPermission = () => {
|
||||
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
return build({ conditionsMatcher });
|
||||
const { rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
return rules;
|
||||
};
|
||||
|
||||
export const orgNoAccessPermissions = buildNoAccessPermission();
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas";
|
||||
import {
|
||||
IdentityProjectMembershipRoleSchema,
|
||||
OrgMembershipsSchema,
|
||||
TableName,
|
||||
TProjectRoles,
|
||||
TProjects
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
@ -10,18 +16,92 @@ export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
|
||||
export const permissionDALFactory = (db: TDbClient) => {
|
||||
const getOrgPermission = async (userId: string, orgId: string) => {
|
||||
try {
|
||||
const groupSubQuery = db(TableName.Groups)
|
||||
.where(`${TableName.Groups}.orgId`, orgId)
|
||||
.join(TableName.UserGroupMembership, (queryBuilder) => {
|
||||
queryBuilder
|
||||
.on(`${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
|
||||
.andOn(`${TableName.UserGroupMembership}.userId`, db.raw("?", [userId]));
|
||||
})
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||
db.ref("orgId").withSchema(TableName.Groups).as("groupOrgId"),
|
||||
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug"),
|
||||
db.ref("role").withSchema(TableName.Groups).as("groupRole"),
|
||||
db.ref("roleId").withSchema(TableName.Groups).as("groupRoleId"),
|
||||
db.ref("createdAt").withSchema(TableName.Groups).as("groupCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.Groups).as("groupUpdatedAt"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles).as("groupCustomRolePermission")
|
||||
);
|
||||
|
||||
const membership = await db
|
||||
.replicaNode()(TableName.OrgMembership)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||
.where("userId", userId)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
|
||||
.select("permissions")
|
||||
.select(selectAllTableCols(TableName.OrgMembership))
|
||||
.first();
|
||||
.where(`${TableName.OrgMembership}.userId`, userId)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`)
|
||||
.leftJoin<Awaited<typeof groupSubQuery>[0]>(
|
||||
groupSubQuery.as("userGroups"),
|
||||
"userGroups.groupOrgId",
|
||||
db.raw("?", [orgId])
|
||||
)
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("groupId").withSchema("userGroups"),
|
||||
db.ref("groupOrgId").withSchema("userGroups"),
|
||||
db.ref("groupName").withSchema("userGroups"),
|
||||
db.ref("groupSlug").withSchema("userGroups"),
|
||||
db.ref("groupRole").withSchema("userGroups"),
|
||||
db.ref("groupRoleId").withSchema("userGroups"),
|
||||
db.ref("groupCreatedAt").withSchema("userGroups"),
|
||||
db.ref("groupUpdatedAt").withSchema("userGroups"),
|
||||
db.ref("groupCustomRolePermission").withSchema("userGroups")
|
||||
);
|
||||
|
||||
return membership;
|
||||
const [formatedDoc] = sqlNestRelationships({
|
||||
data: membership,
|
||||
key: "id",
|
||||
parentMapper: (el) =>
|
||||
OrgMembershipsSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
}).parse(el),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "groupId",
|
||||
label: "groups" as const,
|
||||
mapper: ({
|
||||
groupId,
|
||||
groupUpdatedAt,
|
||||
groupCreatedAt,
|
||||
groupRole,
|
||||
groupRoleId,
|
||||
groupCustomRolePermission,
|
||||
groupName,
|
||||
groupSlug,
|
||||
groupOrgId
|
||||
}) => ({
|
||||
id: groupId,
|
||||
updatedAt: groupUpdatedAt,
|
||||
createdAt: groupCreatedAt,
|
||||
role: groupRole,
|
||||
roleId: groupRoleId,
|
||||
customRolePermission: groupCustomRolePermission,
|
||||
name: groupName,
|
||||
slug: groupSlug,
|
||||
orgId: groupOrgId
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return formatedDoc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetOrgPermission" });
|
||||
}
|
||||
@ -47,74 +127,31 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
|
||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||
try {
|
||||
const groups: string[] = await db
|
||||
.replicaNode()(TableName.GroupProjectMembership)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
||||
|
||||
const groupDocs = await db
|
||||
.replicaNode()(TableName.UserGroupMembership)
|
||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
|
||||
.join(
|
||||
TableName.GroupProjectMembership,
|
||||
`${TableName.GroupProjectMembership}.groupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.join(
|
||||
const docs = await db
|
||||
.replicaNode()(TableName.Users)
|
||||
.where(`${TableName.Users}.id`, userId)
|
||||
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
|
||||
})
|
||||
.leftJoin(
|
||||
TableName.GroupProjectMembershipRole,
|
||||
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||
`${TableName.GroupProjectMembership}.id`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
.leftJoin<TProjectRoles>(
|
||||
{ groupCustomRoles: TableName.ProjectRoles },
|
||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
`groupCustomRoles.id`
|
||||
)
|
||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
|
||||
.leftJoin(TableName.ProjectMembership, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
|
||||
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
|
||||
})
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.GroupProjectMembership}.projectId`,
|
||||
`${TableName.Project}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles).as("permissions"),
|
||||
// db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("apPermissions")
|
||||
// Additional Privileges
|
||||
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
|
||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
||||
|
||||
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
|
||||
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
|
||||
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessEndTime")
|
||||
);
|
||||
// .select(`${TableName.ProjectRoles}.permissions`);
|
||||
|
||||
const docs = await db(TableName.ProjectMembership)
|
||||
.join(
|
||||
TableName.ProjectUserMembershipRole,
|
||||
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
|
||||
`${TableName.ProjectMembership}.id`
|
||||
@ -124,176 +161,229 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.ProjectUserAdditionalPrivilege}.projectId`,
|
||||
`${TableName.ProjectMembership}.projectId`
|
||||
)
|
||||
|
||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
|
||||
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
|
||||
})
|
||||
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
.where(`${TableName.ProjectMembership}.userId`, userId)
|
||||
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
// groups specific
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
|
||||
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
|
||||
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
|
||||
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
|
||||
db
|
||||
.ref("customRoleId")
|
||||
.withSchema(TableName.GroupProjectMembershipRole)
|
||||
.as("userGroupProjectMembershipRoleCustomRoleId"),
|
||||
db
|
||||
.ref("isTemporary")
|
||||
.withSchema(TableName.GroupProjectMembershipRole)
|
||||
.as("userGroupProjectMembershipRoleIsTemporary"),
|
||||
db
|
||||
.ref("temporaryMode")
|
||||
.withSchema(TableName.GroupProjectMembershipRole)
|
||||
.as("userGroupProjectMembershipRoleTemporaryMode"),
|
||||
db
|
||||
.ref("temporaryRange")
|
||||
.withSchema(TableName.GroupProjectMembershipRole)
|
||||
.as("userGroupProjectMembershipRoleTemporaryRange"),
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.GroupProjectMembershipRole)
|
||||
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.GroupProjectMembershipRole)
|
||||
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
|
||||
// user specific
|
||||
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
|
||||
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||
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("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
|
||||
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
|
||||
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
|
||||
db
|
||||
.ref("temporaryMode")
|
||||
.withSchema(TableName.ProjectUserMembershipRole)
|
||||
.as("userProjectMembershipRoleTemporaryMode"),
|
||||
db
|
||||
.ref("isTemporary")
|
||||
.withSchema(TableName.ProjectUserMembershipRole)
|
||||
.as("userProjectMembershipRoleIsTemporary"),
|
||||
db
|
||||
.ref("temporaryRange")
|
||||
.withSchema(TableName.ProjectUserMembershipRole)
|
||||
.as("userProjectMembershipRoleTemporaryRange"),
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.ProjectUserMembershipRole)
|
||||
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.ProjectUserMembershipRole)
|
||||
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
|
||||
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
|
||||
db
|
||||
.ref("permissions")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userAdditionalPrivilegesPermissions"),
|
||||
db
|
||||
.ref("temporaryMode")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userAdditionalPrivilegesTemporaryMode"),
|
||||
db
|
||||
.ref("isTemporary")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userAdditionalPrivilegesIsTemporary"),
|
||||
db
|
||||
.ref("temporaryRange")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userAdditionalPrivilegesTemporaryRange"),
|
||||
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessStartTime"),
|
||||
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userApTemporaryAccessEndTime")
|
||||
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||
// general
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
);
|
||||
|
||||
const permission = sqlNestRelationships({
|
||||
const [userPermission] = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "projectId",
|
||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||
parentMapper: ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
membershipId,
|
||||
groupMembershipId,
|
||||
membershipCreatedAt,
|
||||
groupMembershipCreatedAt,
|
||||
groupMembershipUpdatedAt,
|
||||
membershipUpdatedAt
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
id: membershipId,
|
||||
projectId,
|
||||
createdAt: membershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt
|
||||
id: membershipId || groupMembershipId,
|
||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "id",
|
||||
label: "roles" as const,
|
||||
mapper: (data) =>
|
||||
ProjectUserMembershipRolesSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
}).parse(data)
|
||||
key: "userGroupProjectMembershipRoleId",
|
||||
label: "userGroupRoles" as const,
|
||||
mapper: ({
|
||||
userGroupProjectMembershipRoleId,
|
||||
userGroupProjectMembershipRole,
|
||||
userGroupProjectMembershipRolePermission,
|
||||
userGroupProjectMembershipRoleCustomRoleSlug,
|
||||
userGroupProjectMembershipRoleIsTemporary,
|
||||
userGroupProjectMembershipRoleTemporaryMode,
|
||||
userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||
userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||
userGroupProjectMembershipRoleTemporaryRange
|
||||
}) => ({
|
||||
id: userGroupProjectMembershipRoleId,
|
||||
role: userGroupProjectMembershipRole,
|
||||
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
|
||||
permissions: userGroupProjectMembershipRolePermission,
|
||||
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
|
||||
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
|
||||
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
|
||||
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
|
||||
isTemporary: userGroupProjectMembershipRoleIsTemporary
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "userApId",
|
||||
key: "userProjectMembershipRoleId",
|
||||
label: "projecMembershiptRoles" as const,
|
||||
mapper: ({
|
||||
userProjectMembershipRoleId,
|
||||
userProjectMembershipRole,
|
||||
userProjectCustomRolePermission,
|
||||
userProjectMembershipRoleIsTemporary,
|
||||
userProjectMembershipRoleTemporaryMode,
|
||||
userProjectMembershipRoleTemporaryRange,
|
||||
userProjectMembershipRoleTemporaryAccessEndTime,
|
||||
userProjectMembershipRoleTemporaryAccessStartTime,
|
||||
userProjectMembershipRoleCustomRoleSlug
|
||||
}) => ({
|
||||
id: userProjectMembershipRoleId,
|
||||
role: userProjectMembershipRole,
|
||||
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
|
||||
permissions: userProjectCustomRolePermission,
|
||||
temporaryRange: userProjectMembershipRoleTemporaryRange,
|
||||
temporaryMode: userProjectMembershipRoleTemporaryMode,
|
||||
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
|
||||
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
|
||||
isTemporary: userProjectMembershipRoleIsTemporary
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "userAdditionalPrivilegesId",
|
||||
label: "additionalPrivileges" as const,
|
||||
mapper: ({
|
||||
userApId,
|
||||
userApPermissions,
|
||||
userApIsTemporary,
|
||||
userApTemporaryMode,
|
||||
userApTemporaryRange,
|
||||
userApTemporaryAccessEndTime,
|
||||
userApTemporaryAccessStartTime
|
||||
userAdditionalPrivilegesId,
|
||||
userAdditionalPrivilegesPermissions,
|
||||
userAdditionalPrivilegesIsTemporary,
|
||||
userAdditionalPrivilegesTemporaryMode,
|
||||
userAdditionalPrivilegesTemporaryRange,
|
||||
userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||
userAdditionalPrivilegesTemporaryAccessStartTime
|
||||
}) => ({
|
||||
id: userApId,
|
||||
permissions: userApPermissions,
|
||||
temporaryRange: userApTemporaryRange,
|
||||
temporaryMode: userApTemporaryMode,
|
||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
||||
isTemporary: userApIsTemporary
|
||||
id: userAdditionalPrivilegesId,
|
||||
permissions: userAdditionalPrivilegesPermissions,
|
||||
temporaryRange: userAdditionalPrivilegesTemporaryRange,
|
||||
temporaryMode: userAdditionalPrivilegesTemporaryMode,
|
||||
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
|
||||
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const groupPermission = groupDocs.length
|
||||
? sqlNestRelationships({
|
||||
data: groupDocs,
|
||||
key: "projectId",
|
||||
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
id: membershipId,
|
||||
projectId,
|
||||
createdAt: membershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "id",
|
||||
label: "roles" as const,
|
||||
mapper: (data) =>
|
||||
ProjectUserMembershipRolesSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
}).parse(data)
|
||||
},
|
||||
{
|
||||
key: "userApId",
|
||||
label: "additionalPrivileges" as const,
|
||||
mapper: ({
|
||||
userApId,
|
||||
userApProjectId,
|
||||
userApUserId,
|
||||
userApPermissions,
|
||||
userApIsTemporary,
|
||||
userApTemporaryMode,
|
||||
userApTemporaryRange,
|
||||
userApTemporaryAccessEndTime,
|
||||
userApTemporaryAccessStartTime
|
||||
}) => ({
|
||||
id: userApId,
|
||||
userId: userApUserId,
|
||||
projectId: userApProjectId,
|
||||
permissions: userApPermissions,
|
||||
temporaryRange: userApTemporaryRange,
|
||||
temporaryMode: userApTemporaryMode,
|
||||
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
||||
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
||||
isTemporary: userApIsTemporary
|
||||
})
|
||||
}
|
||||
]
|
||||
})
|
||||
: [];
|
||||
|
||||
if (!permission?.[0] && !groupPermission[0]) return undefined;
|
||||
if (!userPermission) return undefined;
|
||||
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined;
|
||||
|
||||
// when introducting cron mode change it here
|
||||
const activeRoles =
|
||||
permission?.[0]?.roles?.filter(
|
||||
userPermission?.projecMembershiptRoles?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
) ?? [];
|
||||
|
||||
const activeGroupRoles =
|
||||
groupPermission?.[0]?.roles?.filter(
|
||||
userPermission?.userGroupRoles?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
) ?? [];
|
||||
|
||||
const activeAdditionalPrivileges =
|
||||
permission?.[0]?.additionalPrivileges?.filter(
|
||||
userPermission?.additionalPrivileges?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime }) =>
|
||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||
) ?? [];
|
||||
|
||||
const activeGroupAdditionalPrivileges =
|
||||
groupPermission?.[0]?.additionalPrivileges?.filter(
|
||||
({ isTemporary, temporaryAccessEndTime, userId: apUserId, projectId: apProjectId }) =>
|
||||
apProjectId === projectId &&
|
||||
apUserId === userId &&
|
||||
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
|
||||
) ?? [];
|
||||
|
||||
return {
|
||||
...(permission[0] || groupPermission[0]),
|
||||
...userPermission,
|
||||
roles: [...activeRoles, ...activeGroupRoles],
|
||||
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
|
||||
additionalPrivileges: activeAdditionalPrivileges
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||
|
@ -20,7 +20,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||
import { TPermissionDALFactory } from "./permission-dal";
|
||||
import { validateOrgSAML } from "./permission-fns";
|
||||
import { TBuildProjectPermissionDTO } from "./permission-types";
|
||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
|
||||
import {
|
||||
buildServiceTokenProjectPermission,
|
||||
projectAdminPermissions,
|
||||
@ -47,26 +47,29 @@ export const permissionServiceFactory = ({
|
||||
serviceTokenDAL,
|
||||
projectDAL
|
||||
}: TPermissionServiceFactoryDep) => {
|
||||
const buildOrgPermission = (role: string, permission?: unknown) => {
|
||||
switch (role) {
|
||||
case OrgMembershipRole.Admin:
|
||||
return orgAdminPermissions;
|
||||
case OrgMembershipRole.Member:
|
||||
return orgMemberPermissions;
|
||||
case OrgMembershipRole.NoAccess:
|
||||
return orgNoAccessPermissions;
|
||||
case OrgMembershipRole.Custom:
|
||||
return createMongoAbility<OrgPermissionSet>(
|
||||
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
|
||||
permission as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
|
||||
),
|
||||
{
|
||||
conditionsMatcher
|
||||
}
|
||||
);
|
||||
default:
|
||||
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
|
||||
}
|
||||
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
|
||||
const rules = orgUserRoles
|
||||
.map(({ role, permissions }) => {
|
||||
switch (role) {
|
||||
case OrgMembershipRole.Admin:
|
||||
return orgAdminPermissions;
|
||||
case OrgMembershipRole.Member:
|
||||
return orgMemberPermissions;
|
||||
case OrgMembershipRole.NoAccess:
|
||||
return orgNoAccessPermissions;
|
||||
case OrgMembershipRole.Custom:
|
||||
return unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
|
||||
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
|
||||
);
|
||||
default:
|
||||
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
|
||||
}
|
||||
})
|
||||
.reduce((curr, prev) => prev.concat(curr), []);
|
||||
|
||||
return createMongoAbility<OrgPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
});
|
||||
};
|
||||
|
||||
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||
@ -129,7 +132,13 @@ export const permissionServiceFactory = ({
|
||||
|
||||
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
||||
|
||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
||||
membership?.groups?.map(({ role, customRolePermission }) => ({
|
||||
role,
|
||||
permissions: customRolePermission
|
||||
})) || []
|
||||
);
|
||||
return { permission: buildOrgPermission(finalPolicyRoles), membership };
|
||||
};
|
||||
|
||||
const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
|
||||
@ -138,7 +147,10 @@ export const permissionServiceFactory = ({
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||
return {
|
||||
permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]),
|
||||
membership
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgPermission = async (
|
||||
@ -169,11 +181,11 @@ export const permissionServiceFactory = ({
|
||||
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||
if (!orgRole) throw new BadRequestError({ message: "Role not found" });
|
||||
return {
|
||||
permission: buildOrgPermission(OrgMembershipRole.Custom, orgRole.permissions),
|
||||
permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
|
||||
role: orgRole
|
||||
};
|
||||
}
|
||||
return { permission: buildOrgPermission(role, []) };
|
||||
return { permission: buildOrgPermission([{ role, permissions: [] }]) };
|
||||
};
|
||||
|
||||
// user permission for a project in an organization
|
||||
@ -334,7 +346,7 @@ export const permissionServiceFactory = ({
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||
if (isCustomRole) {
|
||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
|
||||
if (!projectRole) throw new BadRequestError({ message: `Role not found: ${role}` });
|
||||
return {
|
||||
permission: buildProjectPermission([
|
||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||
|
@ -2,3 +2,8 @@ export type TBuildProjectPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
||||
|
||||
export type TBuildOrgPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
export enum ProjectPermissionActions {
|
||||
Read = "read",
|
||||
@ -75,117 +76,127 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
|
||||
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
||||
|
||||
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
|
||||
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions.
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
|
||||
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
|
||||
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||
];
|
||||
|
||||
const buildAdminPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
// Admins get full access to everything
|
||||
fullProjectPermissionSet.forEach((permission) => {
|
||||
const [action, subject] = permission;
|
||||
can(action, subject);
|
||||
});
|
||||
|
||||
return rules;
|
||||
};
|
||||
@ -372,4 +383,31 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
||||
return set1.size >= set2.size;
|
||||
};
|
||||
|
||||
/*
|
||||
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
|
||||
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set.
|
||||
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
|
||||
*/
|
||||
export const validateProjectPermissions = (permissions: unknown) => {
|
||||
const parsedPermissions =
|
||||
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
|
||||
|
||||
const flattenedPermissions = [...parsedPermissions];
|
||||
|
||||
for (const perm of flattenedPermissions) {
|
||||
const [action, subject] = perm;
|
||||
|
||||
if (
|
||||
!fullProjectPermissionSet.find(
|
||||
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
|
||||
)
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Permission action ${action} on subject ${subject} is not valid`,
|
||||
name: "Create Role"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-enable */
|
||||
|
@ -75,7 +75,14 @@ type TScimServiceFactoryDep = {
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update"
|
||||
| "create"
|
||||
| "findOne"
|
||||
| "findAllGroupPossibleMembers"
|
||||
| "delete"
|
||||
| "findGroups"
|
||||
| "transaction"
|
||||
| "updateById"
|
||||
| "update"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
@ -775,7 +782,7 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const users = await groupDAL.findAllGroupMembers({
|
||||
const users = await groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id
|
||||
});
|
||||
|
@ -1,33 +1,62 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas";
|
||||
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies, TUsers } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||
|
||||
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
|
||||
|
||||
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
|
||||
|
||||
const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
|
||||
const secretApprovalPolicyFindQuery = (
|
||||
tx: Knex,
|
||||
filter: TFindFilter<TSecretApprovalPolicies>,
|
||||
customFilter?: {
|
||||
sapId?: string;
|
||||
}
|
||||
) =>
|
||||
tx(TableName.SecretApprovalPolicy)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.where((qb) => {
|
||||
if (customFilter?.sapId) {
|
||||
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
|
||||
}
|
||||
})
|
||||
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||
"secretApprovalPolicyApproverUser.id"
|
||||
)
|
||||
.leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${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")
|
||||
tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName")
|
||||
)
|
||||
.select(
|
||||
tx.ref("approverGroupId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
tx.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
|
||||
tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"),
|
||||
tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName")
|
||||
)
|
||||
.select(
|
||||
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||
@ -55,11 +84,31 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
|
||||
userId: approverUserId,
|
||||
email: approverEmail,
|
||||
firstName: approverFirstName,
|
||||
lastName: approverLastName
|
||||
mapper: ({
|
||||
approverUserId: userId,
|
||||
approverEmail: email,
|
||||
approverFirstName: firstName,
|
||||
approverLastName: lastName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({
|
||||
approverGroupUserId: userId,
|
||||
approverGroupEmail: email,
|
||||
approverGroupFirstName: firstName,
|
||||
approverGroupLastName: lastName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -71,9 +120,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
const find = async (
|
||||
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
|
||||
customFilter?: {
|
||||
sapId?: string;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
||||
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
@ -83,11 +138,35 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
...SecretApprovalPoliciesSchema.parse(data)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId: id, approverUsername }) => ({
|
||||
type: ApproverType.User,
|
||||
name: approverUsername,
|
||||
id
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupId: id }) => ({
|
||||
type: ApproverType.Group,
|
||||
id
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverUserId }) => ({
|
||||
userId: approverUserId
|
||||
mapper: ({ approverUserId: userId }) => ({
|
||||
userId
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "userApprovers" as const,
|
||||
mapper: ({ approverGroupUserId: userId }) => ({
|
||||
userId
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -3,11 +3,13 @@ import picomatch from "picomatch";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||
@ -15,6 +17,7 @@ import {
|
||||
TCreateSapDTO,
|
||||
TDeleteSapDTO,
|
||||
TGetBoardSapDTO,
|
||||
TGetSapByIdDTO,
|
||||
TListSapDTO,
|
||||
TUpdateSapDTO
|
||||
} from "./secret-approval-policy-types";
|
||||
@ -28,6 +31,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@ -39,6 +43,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
permissionService,
|
||||
secretApprovalPolicyApproverDAL,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
licenseService
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createSecretApprovalPolicy = async ({
|
||||
@ -54,7 +59,19 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
environment,
|
||||
enforcementLevel
|
||||
}: TCreateSapDTO) => {
|
||||
if (approvals > approvers.length)
|
||||
const groupApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id);
|
||||
const userApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (!groupApprovers.length && approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -91,15 +108,48 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
let userApproverIds = userApprovers;
|
||||
if (userApproverNames.length) {
|
||||
const approverUsers = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames?.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((approverUserId) => ({
|
||||
userApproverIds.map((approverUserId) => ({
|
||||
approverUserId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((approverGroupId) => ({
|
||||
approverGroupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
|
||||
return { ...secretApproval, environment: env, projectId };
|
||||
};
|
||||
|
||||
@ -115,6 +165,18 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
secretPolicyId,
|
||||
enforcementLevel
|
||||
}: TUpdateSapDTO) => {
|
||||
const groupApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
.map((approver) => approver.id);
|
||||
const userApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.User)
|
||||
.map((approver) => approver.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
@ -146,16 +208,52 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
|
||||
if (approvers) {
|
||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
let userApproverIds = userApprovers;
|
||||
if (userApproverNames) {
|
||||
const approverUsers = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
username: userApproverNames
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const approverNamesFromDb = approverUsers.map((user) => user.username);
|
||||
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames?.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||
}
|
||||
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
approvers.map((approverUserId) => ({
|
||||
userApproverIds.map((approverUserId) => ({
|
||||
approverUserId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupApprovers) {
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
groupApprovers.map((approverGroupId) => ({
|
||||
approverGroupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
@ -260,12 +358,41 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
return getSecretApprovalPolicy(projectId, environment, secretPath);
|
||||
};
|
||||
|
||||
const getSecretApprovalPolicyById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
sapId
|
||||
}: TGetSapByIdDTO) => {
|
||||
const [sapPolicy] = await secretApprovalPolicyDAL.find({}, { sapId });
|
||||
|
||||
if (!sapPolicy) {
|
||||
throw new NotFoundError({
|
||||
message: "Cannot find secret approval policy"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
sapPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
return sapPolicy;
|
||||
};
|
||||
|
||||
return {
|
||||
createSecretApprovalPolicy,
|
||||
updateSecretApprovalPolicy,
|
||||
deleteSecretApprovalPolicy,
|
||||
getSecretApprovalPolicy,
|
||||
getSecretApprovalPolicyByProjectId,
|
||||
getSecretApprovalPolicyOfFolder
|
||||
getSecretApprovalPolicyOfFolder,
|
||||
getSecretApprovalPolicyById
|
||||
};
|
||||
};
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
|
||||
|
||||
export type TCreateSapDTO = {
|
||||
approvals: number;
|
||||
secretPath?: string | null;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
projectId: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
@ -14,7 +16,7 @@ export type TUpdateSapDTO = {
|
||||
secretPolicyId: string;
|
||||
approvals?: number;
|
||||
secretPath?: string | null;
|
||||
approvers: string[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@ -25,6 +27,8 @@ export type TDeleteSapDTO = {
|
||||
|
||||
export type TListSapDTO = TProjectPermission;
|
||||
|
||||
export type TGetSapByIdDTO = Omit<TProjectPermission, "projectId"> & { sapId: string };
|
||||
|
||||
export type TGetBoardSapDTO = {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
|
@ -48,16 +48,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||
`committerUser.id`
|
||||
)
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.join<TUsers>(
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||
"secretApprovalPolicyApproverUser.id"
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"),
|
||||
`${TableName.UserGroupMembership}.userId`,
|
||||
`secretApprovalPolicyGroupApproverUser.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
@ -71,10 +81,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
||||
tx.ref("firstName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
|
||||
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
|
||||
tx.ref("lastName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
|
||||
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
|
||||
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
|
||||
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
|
||||
@ -152,13 +167,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({
|
||||
approverUserId,
|
||||
approverUserId: userId,
|
||||
approverEmail: email,
|
||||
approverUsername: username,
|
||||
approverLastName: lastName,
|
||||
approverFirstName: firstName
|
||||
}) => ({
|
||||
userId: approverUserId,
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({
|
||||
approverGroupUserId: userId,
|
||||
approverGroupEmail: email,
|
||||
approverGroupUsername: username,
|
||||
approverGroupLastName: lastName,
|
||||
approverGroupFirstName: firstName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
@ -236,11 +268,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("committerUser"),
|
||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||
@ -269,6 +306,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
void bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
@ -289,6 +327,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
@ -334,7 +373,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId }) => approverUserId
|
||||
mapper: ({ approverUserId }) => ({ userId: approverUserId })
|
||||
},
|
||||
{
|
||||
key: "commitId",
|
||||
@ -344,6 +383,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
secretId
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId })
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -371,11 +415,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("committerUser"),
|
||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||
@ -404,6 +453,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
void bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
@ -424,6 +474,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
|
||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
@ -469,7 +520,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId }) => approverUserId
|
||||
mapper: ({ approverUserId }) => ({ userId: approverUserId })
|
||||
},
|
||||
{
|
||||
key: "commitId",
|
||||
@ -479,6 +530,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
secretId
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupUserId }) => ({
|
||||
userId: approverGroupUserId
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -47,6 +47,9 @@ import {
|
||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
|
||||
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@ -89,7 +92,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<
|
||||
TProjectDALFactory,
|
||||
@ -104,6 +107,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
@ -132,7 +136,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
projectSlackConfigDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
@ -442,8 +447,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
const hasMinApproval =
|
||||
secretApprovalRequest.policy.approvals <=
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
secretApprovalRequest.policy.approvers.filter(({ userId: approverId }) =>
|
||||
approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false
|
||||
).length;
|
||||
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
|
||||
@ -800,7 +805,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
id: policy.approvers.map((approver: { userId: string }) => approver.userId)
|
||||
id: policy.approvers.map((approver: { userId: string | null | undefined }) => approver.userId!)
|
||||
}
|
||||
});
|
||||
|
||||
@ -1069,6 +1074,25 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return { ...doc, commits: approvalCommits };
|
||||
});
|
||||
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||
await triggerSlackNotification({
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await sendApprovalEmailsFn({
|
||||
projectDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
@ -1331,6 +1355,25 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return { ...doc, commits: approvalCommits };
|
||||
});
|
||||
|
||||
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
await triggerSlackNotification({
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL,
|
||||
notification: {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL,
|
||||
payload: {
|
||||
userEmail: user.email as string,
|
||||
environment: env.name,
|
||||
secretPath,
|
||||
projectId,
|
||||
requestId: secretApprovalRequest.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await sendApprovalEmailsFn({
|
||||
projectDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
|
@ -5,26 +5,30 @@ export const GROUPS = {
|
||||
role: "The role of the group to create."
|
||||
},
|
||||
UPDATE: {
|
||||
currentSlug: "The current slug of the group to update.",
|
||||
id: "The id of the group to update",
|
||||
name: "The new name of the group to update to.",
|
||||
slug: "The new slug of the group to update to.",
|
||||
role: "The new role of the group to update to."
|
||||
},
|
||||
DELETE: {
|
||||
id: "The id of the group to delete",
|
||||
slug: "The slug of the group to delete"
|
||||
},
|
||||
LIST_USERS: {
|
||||
slug: "The slug of the group to list users for",
|
||||
id: "The id of the group to list users for",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
|
||||
limit: "The number of users to return.",
|
||||
username: "The username to search for."
|
||||
},
|
||||
ADD_USER: {
|
||||
slug: "The slug of the group to add the user to.",
|
||||
id: "The id of the group to add the user to.",
|
||||
username: "The username of the user to add to the group."
|
||||
},
|
||||
GET_BY_ID: {
|
||||
id: "The id of the group to fetch"
|
||||
},
|
||||
DELETE_USER: {
|
||||
slug: "The slug of the group to remove the user from.",
|
||||
id: "The id of the group to remove the user from.",
|
||||
username: "The username of the user to remove from the group."
|
||||
}
|
||||
} as const;
|
||||
@ -363,7 +367,12 @@ export const ORGANIZATIONS = {
|
||||
membershipId: "The ID of the membership to delete."
|
||||
},
|
||||
LIST_IDENTITY_MEMBERSHIPS: {
|
||||
orgId: "The ID of the organization to get identity memberships from."
|
||||
orgId: "The ID of the organization to get identity memberships from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
|
||||
limit: "The number of identity memberships to return.",
|
||||
orderBy: "The column to order identity memberships by.",
|
||||
orderDirection: "The direction identity memberships will be sorted in.",
|
||||
search: "The text string that identity membership names will be filtered by."
|
||||
},
|
||||
GET_PROJECTS: {
|
||||
organizationId: "The ID of the organization to get projects from."
|
||||
@ -404,21 +413,21 @@ export const PROJECTS = {
|
||||
secretSnapshotId: "The ID of the snapshot to rollback to."
|
||||
},
|
||||
ADD_GROUP_TO_PROJECT: {
|
||||
projectSlug: "The slug of the project to add the group to.",
|
||||
groupSlug: "The slug of the group to add to the project.",
|
||||
projectId: "The ID of the project to add the group to.",
|
||||
groupId: "The ID of the group to add to the project.",
|
||||
role: "The role for the group to assume in the project."
|
||||
},
|
||||
UPDATE_GROUP_IN_PROJECT: {
|
||||
projectSlug: "The slug of the project to update the group in.",
|
||||
groupSlug: "The slug of the group to update in the project.",
|
||||
projectId: "The ID of the project to update the group in.",
|
||||
groupId: "The ID of the group to update in the project.",
|
||||
roles: "A list of roles to update the group to."
|
||||
},
|
||||
REMOVE_GROUP_FROM_PROJECT: {
|
||||
projectSlug: "The slug of the project to delete the group from.",
|
||||
groupSlug: "The slug of the group to delete from the project."
|
||||
projectId: "The ID of the project to delete the group from.",
|
||||
groupId: "The ID of the group to delete from the project."
|
||||
},
|
||||
LIST_GROUPS_IN_PROJECT: {
|
||||
projectSlug: "The slug of the project to list groups for."
|
||||
projectId: "The ID of the project to list groups for."
|
||||
},
|
||||
LIST_INTEGRATION: {
|
||||
workspaceId: "The ID of the project to list integrations for."
|
||||
@ -447,7 +456,9 @@ export const PROJECT_USERS = {
|
||||
INVITE_MEMBER: {
|
||||
projectId: "The ID of the project to invite the member to.",
|
||||
emails: "A list of organization member emails to invite to the project.",
|
||||
usernames: "A list of usernames to invite to the project."
|
||||
usernames: "A list of usernames to invite to the project.",
|
||||
roleSlugs:
|
||||
"A list of role slugs to assign to the newly created project membership. If nothing is provided, it will default to the Member role."
|
||||
},
|
||||
REMOVE_MEMBER: {
|
||||
projectId: "The ID of the project to remove the member from.",
|
||||
@ -470,7 +481,12 @@ export const PROJECT_USERS = {
|
||||
|
||||
export const PROJECT_IDENTITIES = {
|
||||
LIST_IDENTITY_MEMBERSHIPS: {
|
||||
projectId: "The ID of the project to get identity memberships from."
|
||||
projectId: "The ID of the project to get identity memberships from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
|
||||
limit: "The number of identity memberships to return.",
|
||||
orderBy: "The column to order identity memberships by.",
|
||||
orderDirection: "The direction identity memberships will be sorted in.",
|
||||
search: "The text string that identity membership names will be filtered by."
|
||||
},
|
||||
GET_IDENTITY_MEMBERSHIP_BY_ID: {
|
||||
identityId: "The ID of the identity to get the membership for.",
|
||||
@ -685,11 +701,46 @@ export const SECRET_IMPORTS = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const DASHBOARD = {
|
||||
SECRET_OVERVIEW_LIST: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
environments:
|
||||
"The slugs of the environments to list secrets/folders from (comma separated, ie 'environments=dev,staging,prod').",
|
||||
secretPath: "The secret path to list secrets/folders from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
|
||||
limit: "The number of secrets/folders to return.",
|
||||
orderBy: "The column to order secrets/folders by.",
|
||||
orderDirection: "The direction to order secrets/folders in.",
|
||||
search: "The text string to filter secret keys and folder names by.",
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders in the response.",
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||
},
|
||||
SECRET_DETAILS_LIST: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
environment: "The slug of the environment to list secrets/folders from.",
|
||||
secretPath: "The secret path to list secrets/folders from.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
|
||||
limit: "The number of secrets/folders to return.",
|
||||
orderBy: "The column to order secrets/folders by.",
|
||||
orderDirection: "The direction to order secrets/folders in.",
|
||||
search: "The text string to filter secret keys and folder names by.",
|
||||
tags: "The tags to filter secrets by (comma separated, ie 'tags=billing,engineering').",
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders in the response.",
|
||||
includeImports: "Whether to include project secret imports in the response.",
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AUDIT_LOGS = {
|
||||
EXPORT: {
|
||||
workspaceId: "The ID of the project to export audit logs from.",
|
||||
projectId:
|
||||
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
||||
eventType: "The type of the event to export.",
|
||||
userAgentType: "Choose which consuming application to export audit logs for.",
|
||||
eventMetadata:
|
||||
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
|
||||
startDate: "The date to start the export from.",
|
||||
endDate: "The date to end the export at.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
|
||||
@ -1037,14 +1088,18 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
maxPathLength:
|
||||
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
|
||||
keyAlgorithm:
|
||||
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA."
|
||||
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA.",
|
||||
requireTemplateForIssuance:
|
||||
"Whether or not certificates for this CA can only be issued through certificate templates."
|
||||
},
|
||||
GET: {
|
||||
caId: "The ID of the CA to get"
|
||||
},
|
||||
UPDATE: {
|
||||
caId: "The ID of the CA to update",
|
||||
status: "The status of the CA to update to. This can be one of active or disabled"
|
||||
status: "The status of the CA to update to. This can be one of active or disabled",
|
||||
requireTemplateForIssuance:
|
||||
"Whether or not certificates for this CA can only be issued through certificate templates."
|
||||
},
|
||||
DELETE: {
|
||||
caId: "The ID of the CA to delete"
|
||||
@ -1067,6 +1122,10 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
certificateChain: "The certificate chain of the CA",
|
||||
serialNumber: "The serial number of the CA certificate"
|
||||
},
|
||||
GET_CERT_BY_ID: {
|
||||
caId: "The ID of the CA to get the CA certificate from",
|
||||
caCertId: "The ID of the CA certificate to get"
|
||||
},
|
||||
GET_CA_CERTS: {
|
||||
caId: "The ID of the CA to get the CA certificates for",
|
||||
certificate: "The certificate body of the CA certificate",
|
||||
@ -1106,11 +1165,15 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
issuingCaCertificate: "The certificate of the issuing CA",
|
||||
certificateChain: "The certificate chain of the issued certificate",
|
||||
privateKey: "The private key of the issued certificate",
|
||||
serialNumber: "The serial number of the issued certificate"
|
||||
serialNumber: "The serial number of the issued certificate",
|
||||
keyUsages: "The key usage extension of the certificate",
|
||||
extendedKeyUsages: "The extended key usage extension of the certificate"
|
||||
},
|
||||
SIGN_CERT: {
|
||||
caId: "The ID of the CA to issue the certificate from",
|
||||
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
|
||||
keyUsages: "The key usage extension of the certificate",
|
||||
extendedKeyUsages: "The extended key usage extension of the certificate",
|
||||
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
|
||||
friendlyName: "A friendly name for the certificate",
|
||||
commonName: "The common name (CN) for the certificate",
|
||||
@ -1160,7 +1223,10 @@ export const CERTIFICATE_TEMPLATES = {
|
||||
name: "The name of the template",
|
||||
commonName: "The regular expression string to use for validating common names",
|
||||
subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
|
||||
ttl: "The max TTL for the template"
|
||||
ttl: "The max TTL for the template",
|
||||
keyUsages: "The key usage constraint or default value for when template is used during certificate issuance",
|
||||
extendedKeyUsages:
|
||||
"The extended key usage constraint or default value for when template is used during certificate issuance"
|
||||
},
|
||||
GET: {
|
||||
certificateTemplateId: "The ID of the certificate template to get"
|
||||
@ -1172,7 +1238,11 @@ export const CERTIFICATE_TEMPLATES = {
|
||||
name: "The updated name of the template",
|
||||
commonName: "The updated regular expression string for validating common names",
|
||||
subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
|
||||
ttl: "The updated max TTL for the template"
|
||||
ttl: "The updated max TTL for the template",
|
||||
keyUsages:
|
||||
"The updated key usage constraint or default value for when template is used during certificate issuance",
|
||||
extendedKeyUsages:
|
||||
"The updated extended key usage constraint or default value for when template is used during certificate issuance"
|
||||
},
|
||||
DELETE: {
|
||||
certificateTemplateId: "The ID of the certificate template to delete"
|
||||
|
@ -146,7 +146,9 @@ const envSchema = z
|
||||
PLAIN_API_KEY: zpStr(z.string().optional()),
|
||||
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert")
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
@ -5,6 +5,7 @@ import nacl from "tweetnacl";
|
||||
import tweetnacl from "tweetnacl-util";
|
||||
|
||||
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||
import { UserEncryption } from "@app/services/user/user-types";
|
||||
|
||||
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
||||
|
||||
@ -36,12 +37,16 @@ export const srpCheckClientProof = async (
|
||||
// Ghost user related:
|
||||
// This functionality is intended for ghost user logic. This happens on the frontend when a user is being created.
|
||||
// We replicate the same functionality on the backend when creating a ghost user.
|
||||
export const generateUserSrpKeys = async (email: string, password: string) => {
|
||||
export const generateUserSrpKeys = async (
|
||||
email: string,
|
||||
password: string,
|
||||
customKeys?: { publicKey: string; privateKey: string }
|
||||
) => {
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array);
|
||||
const privateKey = customKeys?.privateKey || tweetnacl.encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = customKeys?.publicKey || tweetnacl.encodeBase64(publicKeyUint8Array);
|
||||
|
||||
// eslint-disable-next-line
|
||||
const client = new jsrp.client();
|
||||
@ -111,7 +116,7 @@ export const getUserPrivateKey = async (
|
||||
| "encryptionVersion"
|
||||
>
|
||||
) => {
|
||||
if (user.encryptionVersion === 1) {
|
||||
if (user.encryptionVersion === UserEncryption.V1) {
|
||||
return decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
@ -119,7 +124,12 @@ export const getUserPrivateKey = async (
|
||||
key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
|
||||
});
|
||||
}
|
||||
if (user.encryptionVersion === 2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag) {
|
||||
if (
|
||||
user.encryptionVersion === UserEncryption.V2 &&
|
||||
user.protectedKey &&
|
||||
user.protectedKeyIV &&
|
||||
user.protectedKeyTag
|
||||
) {
|
||||
const derivedKey = await argon2.hash(password, {
|
||||
salt: Buffer.from(user.salt),
|
||||
memoryCost: 65536,
|
||||
|
@ -9,3 +9,8 @@ export const removeTrailingSlash = (str: string) => {
|
||||
|
||||
return str.endsWith("/") ? str.slice(0, -1) : str;
|
||||
};
|
||||
|
||||
export const prefixWithSlash = (str: string) => {
|
||||
if (str.startsWith("/")) return str;
|
||||
return `/${str}`;
|
||||
};
|
||||
|
@ -51,11 +51,17 @@ export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean
|
||||
: unknown)
|
||||
>;
|
||||
|
||||
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
|
||||
export type TFindOpt<
|
||||
R extends object = object,
|
||||
TCount extends boolean = boolean,
|
||||
TCountDistinct extends keyof R | undefined = undefined
|
||||
> = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
||||
groupBy?: keyof R;
|
||||
count?: TCount;
|
||||
countDistinct?: TCountDistinct;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
@ -86,13 +92,18 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
},
|
||||
find: async <TCount extends boolean = false>(
|
||||
find: async <
|
||||
TCount extends boolean = false,
|
||||
TCountDistinct extends keyof Tables[Tname]["base"] | undefined = undefined
|
||||
>(
|
||||
filter: TFindFilter<Tables[Tname]["base"]>,
|
||||
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
|
||||
{ offset, limit, sort, count, tx, countDistinct }: TFindOpt<Tables[Tname]["base"], TCount, TCountDistinct> = {}
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
|
||||
if (count) {
|
||||
if (countDistinct) {
|
||||
void query.countDistinct(countDistinct);
|
||||
} else if (count) {
|
||||
void query.select(db.raw("COUNT(*) OVER() AS count"));
|
||||
void query.select("*");
|
||||
}
|
||||
@ -101,7 +112,8 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
const res = (await query) as TFindReturn<typeof query, TCount>;
|
||||
|
||||
const res = (await query) as TFindReturn<typeof query, TCountDistinct extends undefined ? TCount : true>;
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
|
@ -52,3 +52,8 @@ export enum SecretSharingAccessType {
|
||||
Anyone = "anyone",
|
||||
Organization = "organization"
|
||||
}
|
||||
|
||||
export enum OrderByDirection {
|
||||
ASC = "asc",
|
||||
DESC = "desc"
|
||||
}
|
||||
|
@ -1,10 +1,16 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
export const isDisposableEmail = async (email: string) => {
|
||||
const emailDomain = email.split("@")[1];
|
||||
export const isDisposableEmail = async (emails: string | string[]) => {
|
||||
const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8");
|
||||
if (Array.isArray(emails)) {
|
||||
return emails.some((email) => {
|
||||
const emailDomain = email.split("@")[1];
|
||||
return disposableEmails.split("\n").includes(emailDomain);
|
||||
});
|
||||
}
|
||||
|
||||
const emailDomain = emails.split("@")[1];
|
||||
if (disposableEmails.split("\n").includes(emailDomain)) return true;
|
||||
return false;
|
||||
};
|
||||
|
@ -7,7 +7,11 @@ import {
|
||||
TScanFullRepoEventPayload,
|
||||
TScanPushEventPayload
|
||||
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||
import { TSyncSecretsDTO } from "@app/services/secret/secret-types";
|
||||
import {
|
||||
TFailedIntegrationSyncEmailsPayload,
|
||||
TIntegrationSyncPayload,
|
||||
TSyncSecretsDTO
|
||||
} from "@app/services/secret/secret-types";
|
||||
|
||||
export enum QueueName {
|
||||
SecretRotation = "secret-rotation",
|
||||
@ -42,6 +46,7 @@ export enum QueueJobs {
|
||||
SecWebhook = "secret-webhook-trigger",
|
||||
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
||||
IntegrationSync = "secret-integration-pull",
|
||||
SendFailedIntegrationSyncEmails = "send-failed-integration-sync-emails",
|
||||
SecretScan = "secret-scan",
|
||||
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
|
||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||
@ -88,16 +93,26 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.SecWebhook;
|
||||
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||
};
|
||||
[QueueName.IntegrationSync]: {
|
||||
name: QueueJobs.IntegrationSync;
|
||||
payload: {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
depth?: number;
|
||||
deDupeQueue?: Record<string, boolean>;
|
||||
};
|
||||
};
|
||||
|
||||
[QueueName.AccessTokenStatusUpdate]:
|
||||
| {
|
||||
name: QueueJobs.IdentityAccessTokenStatusUpdate;
|
||||
payload: { identityAccessTokenId: string; numberOfUses: number };
|
||||
}
|
||||
| {
|
||||
name: QueueJobs.ServiceTokenStatusUpdate;
|
||||
payload: { serviceTokenId: string };
|
||||
};
|
||||
|
||||
[QueueName.IntegrationSync]:
|
||||
| {
|
||||
name: QueueJobs.IntegrationSync;
|
||||
payload: TIntegrationSyncPayload;
|
||||
}
|
||||
| {
|
||||
name: QueueJobs.SendFailedIntegrationSyncEmails;
|
||||
payload: TFailedIntegrationSyncEmailsPayload;
|
||||
};
|
||||
[QueueName.SecretFullRepoScan]: {
|
||||
name: QueueJobs.SecretScan;
|
||||
payload: TScanFullRepoEventPayload;
|
||||
@ -151,15 +166,6 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.ProjectV3Migration;
|
||||
payload: { projectId: string };
|
||||
};
|
||||
[QueueName.AccessTokenStatusUpdate]:
|
||||
| {
|
||||
name: QueueJobs.IdentityAccessTokenStatusUpdate;
|
||||
payload: { identityAccessTokenId: string; numberOfUses: number };
|
||||
}
|
||||
| {
|
||||
name: QueueJobs.ServiceTokenStatusUpdate;
|
||||
payload: { serviceTokenId: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import fastifyPlugin from "fastify-plugin";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import {
|
||||
@ -11,6 +12,12 @@ import {
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
|
||||
enum JWTErrors {
|
||||
JwtExpired = "jwt expired",
|
||||
JwtMalformed = "jwt malformed",
|
||||
InvalidAlgorithm = "invalid algorithm"
|
||||
}
|
||||
|
||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||
server.setErrorHandler((error, req, res) => {
|
||||
req.log.error(error);
|
||||
@ -36,6 +43,27 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
status: error.status,
|
||||
detail: error.detail
|
||||
});
|
||||
// Handle JWT errors and make them more human-readable for the end-user.
|
||||
} else if (error instanceof jwt.JsonWebTokenError) {
|
||||
const message = (() => {
|
||||
if (error.message === JWTErrors.JwtExpired) {
|
||||
return "Your token has expired. Please re-authenticate.";
|
||||
}
|
||||
if (error.message === JWTErrors.JwtMalformed) {
|
||||
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
|
||||
}
|
||||
if (error.message === JWTErrors.InvalidAlgorithm) {
|
||||
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
|
||||
}
|
||||
|
||||
return error.message;
|
||||
})();
|
||||
|
||||
void res.status(401).send({
|
||||
statusCode: 401,
|
||||
error: "TokenError",
|
||||
message
|
||||
});
|
||||
} else {
|
||||
void res.send(error);
|
||||
}
|
||||
|
@ -182,6 +182,9 @@ import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/
|
||||
import { secretVersionV2TagBridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
||||
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
||||
import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
|
||||
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||
@ -194,6 +197,8 @@ import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
import { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||
import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
|
||||
import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||
import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal";
|
||||
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||
|
||||
import { injectAuditLogInfo } from "../plugins/audit-log";
|
||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
@ -322,6 +327,10 @@ export const registerRoutes = async (
|
||||
const externalKmsDAL = externalKmsDALFactory(db);
|
||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||
|
||||
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
||||
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@ -371,7 +380,8 @@ export const registerRoutes = async (
|
||||
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||
permissionService,
|
||||
secretApprovalPolicyDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
userDAL
|
||||
});
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
||||
|
||||
@ -464,11 +474,13 @@ export const registerRoutes = async (
|
||||
userAliasDAL,
|
||||
orgMembershipDAL,
|
||||
tokenService,
|
||||
permissionService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
projectMembershipDAL
|
||||
});
|
||||
|
||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
|
||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL });
|
||||
const passwordService = authPaswordServiceFactory({
|
||||
tokenService,
|
||||
smtpService,
|
||||
@ -482,12 +494,12 @@ export const registerRoutes = async (
|
||||
orgRoleDAL,
|
||||
permissionService,
|
||||
orgDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
incidentContactDAL,
|
||||
tokenService,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectRoleDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
orgMembershipDAL,
|
||||
@ -520,8 +532,10 @@ export const registerRoutes = async (
|
||||
serverCfgDAL: superAdminDAL,
|
||||
orgService,
|
||||
keyStore,
|
||||
licenseService
|
||||
licenseService,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
@ -721,7 +735,9 @@ export const registerRoutes = async (
|
||||
keyStore,
|
||||
kmsService,
|
||||
projectBotDAL,
|
||||
certificateTemplateDAL
|
||||
certificateTemplateDAL,
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@ -795,6 +811,8 @@ export const registerRoutes = async (
|
||||
projectEnvDAL,
|
||||
webhookDAL,
|
||||
orgDAL,
|
||||
auditLogService,
|
||||
userDAL,
|
||||
projectMembershipDAL,
|
||||
smtpService,
|
||||
projectDAL,
|
||||
@ -872,7 +890,8 @@ export const registerRoutes = async (
|
||||
smtpService,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
projectSlackConfigDAL
|
||||
});
|
||||
|
||||
const secretService = secretServiceFactory({
|
||||
@ -905,10 +924,12 @@ export const registerRoutes = async (
|
||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
groupDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectMembershipDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const accessApprovalRequestService = accessApprovalRequestServiceFactory({
|
||||
@ -922,7 +943,10 @@ export const registerRoutes = async (
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
smtpService,
|
||||
accessApprovalPolicyApproverDAL
|
||||
accessApprovalPolicyApproverDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService,
|
||||
groupDAL
|
||||
});
|
||||
|
||||
const secretReplicationService = secretReplicationServiceFactory({
|
||||
@ -1150,6 +1174,18 @@ export const registerRoutes = async (
|
||||
userDAL
|
||||
});
|
||||
|
||||
const slackService = slackServiceFactory({
|
||||
permissionService,
|
||||
kmsService,
|
||||
slackIntegrationDAL,
|
||||
workflowIntegrationDAL
|
||||
});
|
||||
|
||||
const workflowIntegrationService = workflowIntegrationServiceFactory({
|
||||
permissionService,
|
||||
workflowIntegrationDAL
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
//
|
||||
// setup the communication with license key server
|
||||
@ -1231,7 +1267,9 @@ export const registerRoutes = async (
|
||||
secretSharing: secretSharingService,
|
||||
userEngagement: userEngagementService,
|
||||
externalKms: externalKmsService,
|
||||
orgAdmin: orgAdminService
|
||||
orgAdmin: orgAdminService,
|
||||
slack: slackService,
|
||||
workflowIntegration: workflowIntegrationService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@ -21,7 +21,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
|
||||
config: SuperAdminSchema.omit({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
encryptedSlackClientId: true,
|
||||
encryptedSlackClientSecret: true
|
||||
}).extend({
|
||||
isMigrationModeOn: z.boolean(),
|
||||
defaultAuthOrgSlug: z.string().nullable(),
|
||||
isSecretScanningDisabled: z.boolean()
|
||||
@ -62,7 +67,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.refine((methods) => !methods || methods.length > 0, {
|
||||
message: "At least one login method should be enabled."
|
||||
})
|
||||
}),
|
||||
slackClientId: z.string().optional(),
|
||||
slackClientSecret: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -123,6 +130,32 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/integrations/slack/config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, res, done) => {
|
||||
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||
verifySuperAdmin(req, res, done);
|
||||
});
|
||||
},
|
||||
handler: async () => {
|
||||
const adminSlackConfig = await server.services.superAdmin.getAdminSlackConfig();
|
||||
|
||||
return adminSlackConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/user-management/users/:userId",
|
||||
|
@ -1,13 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||
import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
@ -42,7 +43,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
keyAlgorithm: z
|
||||
.nativeEnum(CertKeyAlgorithm)
|
||||
.default(CertKeyAlgorithm.RSA_2048)
|
||||
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
||||
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
|
||||
requireTemplateForIssuance: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@ -135,6 +140,33 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// this endpoint will be used to serve the CA certificate when a client makes a request
|
||||
// against the Authority Information Access CA Issuer URL
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/certificates/:caCertId/der",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get DER-encoded certificate of CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caId),
|
||||
caCertId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caCertId)
|
||||
}),
|
||||
response: {
|
||||
200: z.instanceof(Buffer)
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const caCert = await server.services.certificateAuthority.getCaCertById(req.params);
|
||||
|
||||
res.header("Content-Type", "application/pkix-cert");
|
||||
|
||||
return Buffer.from(caCert.rawData);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:caId",
|
||||
@ -148,7 +180,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId)
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status)
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status),
|
||||
requireTemplateForIssuance: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -565,7 +601,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter),
|
||||
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@ -645,7 +683,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter),
|
||||
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@ -700,6 +740,51 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/certificate-templates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get list of certificate templates for the CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplates: CertificateTemplatesSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificateTemplates, ca } = await server.services.certificateAuthority.getCaCertificateTemplates({
|
||||
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_CERTIFICATE_TEMPLATES,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificateTemplates
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/crls",
|
||||
|
@ -7,7 +7,7 @@ import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CrlReason } from "@app/services/certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
@ -86,7 +86,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@ -177,7 +187,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.extendedKeyUsages)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
|
@ -7,6 +7,7 @@ import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
@ -74,7 +75,19 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl)
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT])
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.default([])
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.extendedKeyUsages)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
@ -130,7 +143,13 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl)
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl),
|
||||
keyUsages: z.nativeEnum(CertKeyUsage).array().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.extendedKeyUsages)
|
||||
}),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||
|
@ -246,12 +246,13 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||
}).array()
|
||||
}).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identities = await server.services.identity.listOrgIdentities({
|
||||
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -259,7 +260,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
orgId: req.query.orgId
|
||||
});
|
||||
|
||||
return { identities };
|
||||
return { identities: identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -29,11 +29,13 @@ import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||
import { registerSecretImportRouter } from "./secret-import-router";
|
||||
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||
import { registerSecretTagRouter } from "./secret-tag-router";
|
||||
import { registerSlackRouter } from "./slack-router";
|
||||
import { registerSsoRouter } from "./sso-router";
|
||||
import { registerUserActionRouter } from "./user-action-router";
|
||||
import { registerUserEngagementRouter } from "./user-engagement-router";
|
||||
import { registerUserRouter } from "./user-router";
|
||||
import { registerWebhookRouter } from "./webhook-router";
|
||||
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
|
||||
|
||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSsoRouter, { prefix: "/sso" });
|
||||
@ -61,6 +63,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSecretImportRouter, { prefix: "/secret-imports" });
|
||||
await server.register(registerSecretFolderRouter, { prefix: "/folders" });
|
||||
|
||||
await server.register(
|
||||
async (workflowIntegrationRouter) => {
|
||||
await workflowIntegrationRouter.register(registerWorkflowIntegrationRouter);
|
||||
await workflowIntegrationRouter.register(registerSlackRouter, { prefix: "/slack" });
|
||||
},
|
||||
{ prefix: "/workflow-integrations" }
|
||||
);
|
||||
|
||||
await server.register(
|
||||
async (projectRouter) => {
|
||||
await projectRouter.register(registerProjectRouter);
|
||||
|
@ -4,7 +4,7 @@ import { IntegrationsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { INTEGRATION } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash, shake } from "@app/lib/fn";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -154,6 +154,48 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:integrationId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get an integration by integration id",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema.extend({
|
||||
environment: z.object({
|
||||
slug: z.string().trim(),
|
||||
name: z.string().trim(),
|
||||
id: z.string().trim()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const integration = await server.services.integration.getIntegration({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationId
|
||||
});
|
||||
|
||||
return { integration };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:integrationId",
|
||||
|
@ -18,9 +18,14 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
inviteeEmails: z.array(z.string().trim().email()),
|
||||
organizationId: z.string().trim(),
|
||||
projectIds: z.array(z.string().trim()).optional(),
|
||||
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
|
||||
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
|
||||
projects: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
projectRoleSlug: z.string().array().default([ProjectMembershipRole.Member])
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
organizationRoleSlug: z.string().default(OrgMembershipRole.Member)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -40,12 +45,12 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
if (req.auth.actor !== ActorType.USER) return;
|
||||
|
||||
const completeInviteLinks = await server.services.org.inviteUserToOrganization({
|
||||
const { signupTokens: completeInviteLinks } = await server.services.org.inviteUserToOrganization({
|
||||
orgId: req.body.organizationId,
|
||||
userId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
inviteeEmails: req.body.inviteeEmails,
|
||||
projectIds: req.body.projectIds,
|
||||
projectRoleSlug: req.body.projectRoleSlug,
|
||||
projects: req.body.projects,
|
||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AuditLogsSchema,
|
||||
GroupsSchema,
|
||||
IncidentContactsSchema,
|
||||
OrganizationsSchema,
|
||||
@ -8,10 +9,14 @@ import {
|
||||
OrgRolesSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getLastMidnightDateISO } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -62,6 +67,109 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/audit-logs",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get all audit logs for an organization",
|
||||
querystring: z.object({
|
||||
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
||||
actorType: z.nativeEnum(ActorType).optional(),
|
||||
// eventType is split with , for multiple values, we need to transform it to array
|
||||
eventType: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val ? val.split(",") : undefined)),
|
||||
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
||||
eventMetadata: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const pairs = val.split(",");
|
||||
|
||||
return pairs.reduce(
|
||||
(acc, pair) => {
|
||||
const [key, value] = pair.split("=");
|
||||
if (key && value) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
})
|
||||
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
|
||||
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
||||
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
||||
}),
|
||||
|
||||
response: {
|
||||
200: z.object({
|
||||
auditLogs: AuditLogsSchema.omit({
|
||||
eventMetadata: true,
|
||||
eventType: true,
|
||||
actor: true,
|
||||
actorMetadata: true
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
}),
|
||||
actor: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
})
|
||||
})
|
||||
)
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.isCloud) {
|
||||
throw new BadRequestError({ message: "Infisical cloud audit log is in maintenance mode." });
|
||||
}
|
||||
|
||||
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||
filter: {
|
||||
...req.query,
|
||||
endDate: req.query.endDate,
|
||||
projectId: req.query.projectId,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActorId: req.query.actor,
|
||||
actorType: req.query.actorType,
|
||||
eventType: req.query.eventType as EventType[] | undefined
|
||||
},
|
||||
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
});
|
||||
return { auditLogs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/users",
|
||||
|
@ -4,14 +4,17 @@ import {
|
||||
IntegrationsSchema,
|
||||
ProjectMembershipsSchema,
|
||||
ProjectRolesSchema,
|
||||
ProjectSlackConfigsSchema,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
|
||||
|
||||
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
|
||||
@ -542,4 +545,111 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
return { serviceTokenData };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/slack-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSlackConfigsSchema.pick({
|
||||
id: true,
|
||||
slackIntegrationId: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackConfig = await server.services.project.getProjectSlackConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
if (slackConfig) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_SLACK_CONFIG,
|
||||
metadata: {
|
||||
id: slackConfig.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return slackConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PUT",
|
||||
url: "/:workspaceId/slack-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
slackIntegrationId: z.string(),
|
||||
isAccessRequestNotificationEnabled: z.boolean(),
|
||||
accessRequestChannels: validateSlackChannelsField,
|
||||
isSecretRequestNotificationEnabled: z.boolean(),
|
||||
secretRequestChannels: validateSlackChannelsField
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSlackConfigsSchema.pick({
|
||||
id: true,
|
||||
slackIntegrationId: true,
|
||||
isAccessRequestNotificationEnabled: true,
|
||||
accessRequestChannels: true,
|
||||
isSecretRequestNotificationEnabled: true,
|
||||
secretRequestChannels: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackConfig = await server.services.project.updateProjectSlackConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_SLACK_CONFIG,
|
||||
metadata: {
|
||||
id: slackConfig.id,
|
||||
slackIntegrationId: slackConfig.slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled: slackConfig.isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels: slackConfig.accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled: slackConfig.isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels: slackConfig.secretRequestChannels
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return slackConfig;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import { SecretFoldersSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { FOLDERS } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -26,9 +26,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId),
|
||||
environment: z.string().trim().describe(FOLDERS.CREATE.environment),
|
||||
name: z.string().trim().describe(FOLDERS.CREATE.name),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.path),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.CREATE.path),
|
||||
// backward compatiability with cli
|
||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.directory)
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.CREATE.directory)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -86,9 +98,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
|
||||
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
|
||||
name: z.string().trim().describe(FOLDERS.UPDATE.name),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.path),
|
||||
// backward compatiability with cli
|
||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.directory)
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.directory)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -147,7 +171,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
id: z.string().describe(FOLDERS.UPDATE.folderId),
|
||||
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
|
||||
name: z.string().trim().describe(FOLDERS.UPDATE.name),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path)
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.path)
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
@ -211,9 +241,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId),
|
||||
environment: z.string().trim().describe(FOLDERS.DELETE.environment),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.path),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.DELETE.path),
|
||||
// keep this here as cli need directory
|
||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.directory)
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.DELETE.directory)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -267,9 +309,21 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId),
|
||||
environment: z.string().trim().describe(FOLDERS.LIST.environment),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.path),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.LIST.path),
|
||||
// backward compatiability with cli
|
||||
directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.directory)
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.LIST.directory)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
355
backend/src/server/routes/v1/slack-router.ts
Normal file
355
backend/src/server/routes/v1/slack-router.ts
Normal file
@ -0,0 +1,355 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const sanitizedSlackIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||
id: true,
|
||||
description: true,
|
||||
slug: true,
|
||||
integration: true
|
||||
}).merge(
|
||||
SlackIntegrationsSchema.pick({
|
||||
teamName: true
|
||||
})
|
||||
);
|
||||
|
||||
export const registerSlackRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/install",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
}),
|
||||
description: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const url = await server.services.slack.getInstallUrl({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
slug: req.query.slug,
|
||||
description: req.query.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/reinstall",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const url = await server.services.slack.getReinstallUrl({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.query.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: req.query.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
response: {
|
||||
200: sanitizedSlackIntegrationSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackIntegrations = await server.services.slack.getSlackIntegrationsByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return slackIntegrations;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSlackIntegrationSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const deletedSlackIntegration = await server.services.slack.deleteSlackIntegration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: deletedSlackIntegration.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: deletedSlackIntegration.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return deletedSlackIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSlackIntegrationSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackIntegration = await server.services.slack.getSlackIntegrationById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: slackIntegration.orgId,
|
||||
event: {
|
||||
type: EventType.GET_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: slackIntegration.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return slackIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id/channels",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackChannels = await server.services.slack.getSlackIntegrationChannels({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
return slackChannels;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.optional(),
|
||||
description: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedSlackIntegrationSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const slackIntegration = await server.services.slack.updateSlackIntegration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: slackIntegration.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SLACK_INTEGRATION,
|
||||
metadata: {
|
||||
id: slackIntegration.id,
|
||||
slug: slackIntegration.slug,
|
||||
description: slackIntegration.description as string
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return slackIntegration;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/oauth_redirect",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const installer = await server.services.slack.getSlackInstaller();
|
||||
|
||||
return installer.handleCallback(req.raw, res.raw, {
|
||||
failureAsync: async () => {
|
||||
return res.redirect(appCfg.SITE_URL as string);
|
||||
},
|
||||
successAsync: async (installation) => {
|
||||
const metadata = JSON.parse(installation.metadata || "") as {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
return res.redirect(`${appCfg.SITE_URL}/org/${metadata.orgId}/settings?selectedTab=workflow-integrations`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -134,4 +134,39 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me/:username/groups",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
username: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const groupMemberships = await server.services.user.listUserGroups({
|
||||
username: req.params.username,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
});
|
||||
|
||||
return groupMemberships;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
42
backend/src/server/routes/v1/workflow-integration-router.ts
Normal file
42
backend/src/server/routes/v1/workflow-integration-router.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { WorkflowIntegrationsSchema } from "@app/db/schemas";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const sanitizedWorkflowIntegrationSchema = WorkflowIntegrationsSchema.pick({
|
||||
id: true,
|
||||
description: true,
|
||||
slug: true,
|
||||
integration: true
|
||||
});
|
||||
|
||||
export const registerWorkflowIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
response: {
|
||||
200: sanitizedWorkflowIntegrationSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const workflowIntegrations = await server.services.workflowIntegration.getIntegrationsByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return workflowIntegrations;
|
||||
}
|
||||
});
|
||||
};
|
@ -8,6 +8,7 @@ import {
|
||||
ProjectUserMembershipRolesSchema
|
||||
} from "@app/db/schemas";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||
@ -15,8 +16,11 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member
|
||||
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:projectSlug/groups/:groupSlug",
|
||||
url: "/:projectId/groups/:groupId",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Add group to project",
|
||||
security: [
|
||||
@ -25,17 +29,39 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug),
|
||||
groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug)
|
||||
}),
|
||||
body: z.object({
|
||||
role: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default(ProjectMembershipRole.NoAccess)
|
||||
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role)
|
||||
projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId),
|
||||
groupId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
role: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default(ProjectMembershipRole.NoAccess)
|
||||
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role),
|
||||
roles: z
|
||||
.array(
|
||||
z.union([
|
||||
z.object({
|
||||
role: z.string(),
|
||||
isTemporary: z.literal(false).default(false)
|
||||
}),
|
||||
z.object({
|
||||
role: z.string(),
|
||||
isTemporary: z.literal(true),
|
||||
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
|
||||
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
|
||||
temporaryAccessStartTime: z.string().datetime()
|
||||
})
|
||||
])
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
.refine((data) => data.role || data.roles, {
|
||||
message: "Either role or roles must be present",
|
||||
path: ["role", "roles"]
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
groupMembership: GroupProjectMembershipsSchema
|
||||
@ -48,17 +74,18 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
groupSlug: req.params.groupSlug,
|
||||
projectSlug: req.params.projectSlug,
|
||||
role: req.body.role
|
||||
roles: req.body.roles || [{ role: req.body.role }],
|
||||
projectId: req.params.projectId,
|
||||
groupId: req.params.groupId
|
||||
});
|
||||
|
||||
return { groupMembership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:projectSlug/groups/:groupSlug",
|
||||
url: "/:projectId/groups/:groupId",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update group in project",
|
||||
@ -68,8 +95,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectSlug),
|
||||
groupSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupSlug)
|
||||
projectId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectId),
|
||||
groupId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupId)
|
||||
}),
|
||||
body: z.object({
|
||||
roles: z
|
||||
@ -103,18 +130,22 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
groupSlug: req.params.groupSlug,
|
||||
projectSlug: req.params.projectSlug,
|
||||
projectId: req.params.projectId,
|
||||
groupId: req.params.groupId,
|
||||
roles: req.body.roles
|
||||
});
|
||||
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:projectSlug/groups/:groupSlug",
|
||||
url: "/:projectId/groups/:groupId",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Remove group from project",
|
||||
security: [
|
||||
@ -123,8 +154,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectSlug),
|
||||
groupSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupSlug)
|
||||
projectId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectId),
|
||||
groupId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -138,17 +169,21 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
groupSlug: req.params.groupSlug,
|
||||
projectSlug: req.params.projectSlug
|
||||
groupId: req.params.groupId,
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
|
||||
return { groupMembership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectSlug/groups",
|
||||
url: "/:projectId/groups",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Return list of groups in project",
|
||||
security: [
|
||||
@ -157,7 +192,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectSlug: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectSlug)
|
||||
projectId: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -193,9 +228,67 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.params.projectSlug
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
|
||||
return { groupMemberships };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/groups/:groupId",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Return project group",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
projectId: z.string().trim(),
|
||||
groupId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
groupMembership: z.object({
|
||||
id: z.string(),
|
||||
groupId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
role: z.string(),
|
||||
customRoleId: z.string().optional().nullable(),
|
||||
customRoleName: z.string().optional().nullable(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryMode: z.string().optional().nullable(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
),
|
||||
group: GroupsSchema.pick({ name: true, id: true, slug: true })
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const groupMembership = await server.services.groupProject.getGroupInProject({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
return { groupMembership };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -2,9 +2,11 @@ import { z } from "zod";
|
||||
|
||||
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
|
||||
|
||||
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -24,6 +26,27 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).default(0).describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.offset).optional(),
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.min(1)
|
||||
.max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added
|
||||
.default(100)
|
||||
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.limit)
|
||||
.optional(),
|
||||
orderBy: z
|
||||
.nativeEnum(OrgIdentityOrderBy)
|
||||
.default(OrgIdentityOrderBy.Name)
|
||||
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
|
||||
.optional(),
|
||||
orderDirection: z
|
||||
.nativeEnum(OrderByDirection)
|
||||
.default(OrderByDirection.ASC)
|
||||
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
|
||||
.optional(),
|
||||
search: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.search).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityMemberships: IdentityOrgMembershipsSchema.merge(
|
||||
@ -37,20 +60,26 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||
})
|
||||
).array()
|
||||
).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityMemberships = await server.services.identity.listOrgIdentities({
|
||||
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.orgId
|
||||
orgId: req.params.orgId,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
orderBy: req.query.orderBy,
|
||||
orderDirection: req.query.orderDirection,
|
||||
search: req.query.search
|
||||
});
|
||||
|
||||
return { identityMemberships };
|
||||
return { identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -7,11 +7,13 @@ import {
|
||||
ProjectMembershipRole,
|
||||
ProjectUserMembershipRolesSchema
|
||||
} from "@app/db/schemas";
|
||||
import { PROJECT_IDENTITIES } from "@app/lib/api-docs";
|
||||
import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types";
|
||||
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||
|
||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
@ -214,6 +216,32 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
params: z.object({
|
||||
projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce
|
||||
.number()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.offset)
|
||||
.optional(),
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.min(1)
|
||||
.max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added
|
||||
.default(100)
|
||||
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.limit)
|
||||
.optional(),
|
||||
orderBy: z
|
||||
.nativeEnum(ProjectIdentityOrderBy)
|
||||
.default(ProjectIdentityOrderBy.Name)
|
||||
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
|
||||
.optional(),
|
||||
orderDirection: z
|
||||
.nativeEnum(OrderByDirection)
|
||||
.default(OrderByDirection.ASC)
|
||||
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
|
||||
.optional(),
|
||||
search: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.search).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityMemberships: z
|
||||
@ -239,19 +267,25 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
})
|
||||
.array()
|
||||
.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityMemberships = await server.services.identityProject.listProjectIdentities({
|
||||
const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.projectId
|
||||
projectId: req.params.projectId,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
orderBy: req.query.orderBy,
|
||||
orderDirection: req.query.orderDirection,
|
||||
search: req.query.search
|
||||
});
|
||||
return { identityMemberships };
|
||||
return { identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PROJECT_USERS } from "@app/lib/api-docs";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -26,7 +26,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
||||
}),
|
||||
body: z.object({
|
||||
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
|
||||
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames)
|
||||
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames),
|
||||
roleSlugs: z.string().array().optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -36,14 +37,21 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
|
||||
projectId: req.params.projectId,
|
||||
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
|
||||
const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
emails: req.body.emails,
|
||||
usernames: req.body.usernames
|
||||
inviteeEmails: usernamesAndEmails,
|
||||
orgId: req.permission.orgId,
|
||||
organizationRoleSlug: OrgMembershipRole.NoAccess,
|
||||
projects: [
|
||||
{
|
||||
id: req.params.projectId,
|
||||
projectRoleSlug: [ProjectMembershipRole.Member]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
|
647
backend/src/server/routes/v3/dashboard-router.ts
Normal file
647
backend/src/server/routes/v3/dashboard-router.ts
Normal file
@ -0,0 +1,647 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { DASHBOARD } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secrets-overview",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List project secrets overview",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.projectId),
|
||||
environments: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform(decodeURIComponent)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.environments),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.secretPath),
|
||||
offset: z.coerce.number().min(0).optional().default(0).describe(DASHBOARD.SECRET_OVERVIEW_LIST.offset),
|
||||
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(DASHBOARD.SECRET_OVERVIEW_LIST.limit),
|
||||
orderBy: z
|
||||
.nativeEnum(SecretsOrderBy)
|
||||
.default(SecretsOrderBy.Name)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderBy)
|
||||
.optional(),
|
||||
orderDirection: z
|
||||
.nativeEnum(OrderByDirection)
|
||||
.default(OrderByDirection.ASC)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
|
||||
.optional(),
|
||||
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
|
||||
includeSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||
includeFolders: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||
includeDynamicSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(),
|
||||
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretPath: z.string().optional(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
totalFolderCount: z.number().optional(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
secretPath,
|
||||
projectId,
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
includeFolders,
|
||||
includeSecrets,
|
||||
includeDynamicSecrets
|
||||
} = req.query;
|
||||
|
||||
const environments = req.query.environments.split(",");
|
||||
|
||||
if (!projectId || environments.length === 0)
|
||||
throw new BadRequestError({ message: "Missing workspace id or environment(s)" });
|
||||
|
||||
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
|
||||
|
||||
// prevent older projects from accessing endpoint
|
||||
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
|
||||
|
||||
let remainingLimit = limit;
|
||||
let adjustedOffset = offset;
|
||||
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
|
||||
let dynamicSecrets:
|
||||
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByFolderIds>>
|
||||
| undefined;
|
||||
|
||||
let totalFolderCount: number | undefined;
|
||||
let totalDynamicSecretCount: number | undefined;
|
||||
let totalSecretCount: number | undefined;
|
||||
|
||||
if (includeFolders) {
|
||||
// this is the unique count, ie duplicate folders across envs only count as 1
|
||||
totalFolderCount = await server.services.folder.getProjectFolderCount({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.projectId,
|
||||
path: secretPath,
|
||||
environments,
|
||||
search
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalFolderCount > adjustedOffset) {
|
||||
folders = await server.services.folder.getFoldersMultiEnv({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
environments,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
});
|
||||
|
||||
// get the count of unique folder names to properly adjust remaining limit
|
||||
const uniqueFolderCount = new Set(folders.map((folder) => folder.name)).size;
|
||||
|
||||
remainingLimit -= uniqueFolderCount;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalFolderCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (!includeDynamicSecrets && !includeSecrets)
|
||||
return {
|
||||
folders,
|
||||
totalFolderCount,
|
||||
totalCount: totalFolderCount ?? 0
|
||||
};
|
||||
|
||||
const { permission } = await server.services.permission.getProjectPermission(
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
projectId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
const permissiveEnvs = // filter envs user has access to
|
||||
environments.filter((environment) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
)
|
||||
);
|
||||
|
||||
if (includeDynamicSecrets) {
|
||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
search,
|
||||
environmentSlugs: permissiveEnvs,
|
||||
path: secretPath,
|
||||
isInternal: true
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
|
||||
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByFolderIds({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environmentSlugs: permissiveEnvs,
|
||||
path: secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
isInternal: true
|
||||
});
|
||||
|
||||
// get the count of unique dynamic secret names to properly adjust remaining limit
|
||||
const uniqueDynamicSecretsCount = new Set(dynamicSecrets.map((dynamicSecret) => dynamicSecret.name)).size;
|
||||
|
||||
remainingLimit -= uniqueDynamicSecretsCount;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeSecrets) {
|
||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environments: permissiveEnvs,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
search,
|
||||
isInternal: true
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environments: permissiveEnvs,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
isInternal: true
|
||||
});
|
||||
|
||||
for await (const environment of permissiveEnvs) {
|
||||
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||
|
||||
if (secretCountFromEnv) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCountFromEnv
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: secretCountFromEnv,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
secrets,
|
||||
totalFolderCount,
|
||||
totalDynamicSecretCount,
|
||||
totalSecretCount,
|
||||
totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secrets-details",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List project secrets details",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.projectId),
|
||||
environment: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.secretPath),
|
||||
offset: z.coerce.number().min(0).optional().default(0).describe(DASHBOARD.SECRET_DETAILS_LIST.offset),
|
||||
limit: z.coerce.number().min(1).max(100).optional().default(100).describe(DASHBOARD.SECRET_DETAILS_LIST.limit),
|
||||
orderBy: z
|
||||
.nativeEnum(SecretsOrderBy)
|
||||
.default(SecretsOrderBy.Name)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.orderBy)
|
||||
.optional(),
|
||||
orderDirection: z
|
||||
.nativeEnum(OrderByDirection)
|
||||
.default(OrderByDirection.ASC)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.orderDirection)
|
||||
.optional(),
|
||||
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
||||
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
||||
includeSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||
includeFolders: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||
includeDynamicSecrets: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||
includeImports: z.coerce
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
imports: SecretImportsSchema.omit({ importEnv: true })
|
||||
.extend({
|
||||
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
folders: SecretFoldersSchema.array().optional(),
|
||||
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretPath: z.string().optional(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
totalImportCount: z.number().optional(),
|
||||
totalFolderCount: z.number().optional(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
secretPath,
|
||||
environment,
|
||||
projectId,
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
includeFolders,
|
||||
includeSecrets,
|
||||
includeDynamicSecrets,
|
||||
includeImports
|
||||
} = req.query;
|
||||
|
||||
if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
|
||||
|
||||
const { shouldUseSecretV2Bridge } = await server.services.projectBot.getBotKey(projectId);
|
||||
|
||||
// prevent older projects from accessing endpoint
|
||||
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
|
||||
|
||||
const tags = req.query.tags?.split(",") ?? [];
|
||||
|
||||
let remainingLimit = limit;
|
||||
let adjustedOffset = offset;
|
||||
|
||||
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined;
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"] | undefined;
|
||||
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
|
||||
|
||||
let totalImportCount: number | undefined;
|
||||
let totalFolderCount: number | undefined;
|
||||
let totalDynamicSecretCount: number | undefined;
|
||||
let totalSecretCount: number | undefined;
|
||||
|
||||
if (includeImports) {
|
||||
totalImportCount = await server.services.secretImport.getProjectImportCount({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
environment,
|
||||
path: secretPath,
|
||||
search
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalImportCount > adjustedOffset) {
|
||||
imports = await server.services.secretImport.getImports({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
environment,
|
||||
path: secretPath,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_IMPORTS,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: imports?.[0]?.folderId,
|
||||
numberOfImports: imports.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
remainingLimit -= imports.length;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalImportCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeFolders) {
|
||||
totalFolderCount = await server.services.folder.getProjectFolderCount({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
environments: [environment],
|
||||
search
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalFolderCount > adjustedOffset) {
|
||||
folders = await server.services.folder.getFolders({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
environment,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
});
|
||||
|
||||
remainingLimit -= folders.length;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalFolderCount);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (includeDynamicSecrets) {
|
||||
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
search,
|
||||
environmentSlug: environment,
|
||||
path: secretPath
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalDynamicSecretCount > adjustedOffset) {
|
||||
dynamicSecrets = await server.services.dynamicSecret.listDynamicSecretsByEnv({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environmentSlug: environment,
|
||||
path: secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
});
|
||||
|
||||
remainingLimit -= dynamicSecrets.length;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeSecrets) {
|
||||
totalSecretCount = await server.services.secret.getSecretsCount({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
search,
|
||||
tagSlugs: tags
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||
const secretsRaw = await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
tagSlugs: tags
|
||||
});
|
||||
|
||||
secrets = secretsRaw.secrets;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secrets.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!(error instanceof ForbiddenError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
imports,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
secrets,
|
||||
totalImportCount,
|
||||
totalFolderCount,
|
||||
totalDynamicSecretCount,
|
||||
totalSecretCount,
|
||||
totalCount:
|
||||
(totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { registerDashboardRouter } from "./dashboard-router";
|
||||
import { registerLoginRouter } from "./login-router";
|
||||
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
||||
import { registerSecretRouter } from "./secret-router";
|
||||
@ -10,4 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerUserRouter, { prefix: "/users" });
|
||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||
};
|
||||
|
@ -42,7 +42,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
organizationId: z.string().trim()
|
||||
organizationId: z.string().trim(),
|
||||
userAgent: z.enum(["cli"]).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -53,7 +54,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req, res) => {
|
||||
const cfg = getConfig();
|
||||
const tokens = await server.services.login.selectOrganization({
|
||||
userAgent: req.headers["user-agent"],
|
||||
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
||||
authJwtToken: req.headers.authorization,
|
||||
organizationId: req.body.organizationId,
|
||||
ipAddress: req.realIp
|
||||
|
@ -12,7 +12,6 @@ import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/erro
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
@ -34,7 +33,6 @@ type TAuthLoginServiceFactoryDep = {
|
||||
orgDAL: TOrgDALFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
tokenDAL: TTokenDALFactory;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@ -42,8 +40,7 @@ export const authLoginServiceFactory = ({
|
||||
userDAL,
|
||||
tokenService,
|
||||
smtpService,
|
||||
orgDAL,
|
||||
tokenDAL
|
||||
orgDAL
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
/*
|
||||
* Private
|
||||
@ -376,8 +373,6 @@ export const authLoginServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId);
|
||||
|
||||
const tokens = await generateUserTokens({
|
||||
authMethod: decodedToken.authMethod,
|
||||
user,
|
||||
|
@ -1,15 +1,15 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas";
|
||||
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -17,14 +17,14 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { UserEncryption } from "../user/user-types";
|
||||
import { TAuthDALFactory } from "./auth-dal";
|
||||
import { validateProviderAuthToken, validateSignUpAuthorization } from "./auth-fns";
|
||||
import { TCompleteAccountInviteDTO, TCompleteAccountSignupDTO } from "./auth-signup-type";
|
||||
@ -67,8 +67,6 @@ export const authSignupServiceFactory = ({
|
||||
smtpService,
|
||||
orgService,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
licenseService
|
||||
}: TAuthSignupDep) => {
|
||||
// first step of signup. create user and send email
|
||||
@ -177,32 +175,88 @@ export const authSignupServiceFactory = ({
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
encryptionVersion: 2
|
||||
encryptionVersion: UserEncryption.V2
|
||||
});
|
||||
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
|
||||
const updateduser = await authDAL.transaction(async (tx) => {
|
||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||
if (!us) throw new Error("User not found");
|
||||
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
salt,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
||||
let userEncKey;
|
||||
|
||||
// below condition is true means this is system generated credentials
|
||||
// the private key is actually system generated password
|
||||
// thus we will re-encrypt the system generated private key with the new password
|
||||
// akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it
|
||||
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things
|
||||
if (
|
||||
systemGeneratedUserEncryptionKey &&
|
||||
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
||||
) {
|
||||
// get server generated password
|
||||
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
||||
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
|
||||
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
|
||||
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
|
||||
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
|
||||
...systemGeneratedUserEncryptionKey
|
||||
});
|
||||
const encKeys = await generateUserSrpKeys(email, password, {
|
||||
publicKey: systemGeneratedUserEncryptionKey.publicKey,
|
||||
privateKey: serverGeneratedPrivateKey
|
||||
});
|
||||
// now reencrypt server generated key with user provided password
|
||||
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
encryptionVersion: UserEncryption.V2,
|
||||
protectedKey: encKeys.protectedKey,
|
||||
protectedKeyIV: encKeys.protectedKeyIV,
|
||||
protectedKeyTag: encKeys.protectedKeyTag,
|
||||
publicKey: encKeys.publicKey,
|
||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||
iv: encKeys.encryptedPrivateKeyIV,
|
||||
tag: encKeys.encryptedPrivateKeyTag,
|
||||
salt: encKeys.salt,
|
||||
verifier: encKeys.verifier,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
} else {
|
||||
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
encryptionVersion: UserEncryption.V2,
|
||||
salt,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
|
||||
if (
|
||||
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
|
||||
@ -312,8 +366,7 @@ export const authSignupServiceFactory = ({
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
authorization,
|
||||
tokenMetadata
|
||||
authorization
|
||||
}: TCompleteAccountInviteDTO) => {
|
||||
const user = await userDAL.findUserByUsername(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
@ -348,65 +401,76 @@ export const authSignupServiceFactory = ({
|
||||
const updateduser = await authDAL.transaction(async (tx) => {
|
||||
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
|
||||
if (!us) throw new Error("User not found");
|
||||
const userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
salt,
|
||||
encryptionVersion: 2,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
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 systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
|
||||
let userEncKey;
|
||||
// this means this is system generated credentials
|
||||
// now replace the private key
|
||||
if (
|
||||
systemGeneratedUserEncryptionKey &&
|
||||
!systemGeneratedUserEncryptionKey.hashedPassword &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
|
||||
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
|
||||
) {
|
||||
// get server generated password
|
||||
const serverGeneratedPassword = infisicalSymmetricDecrypt({
|
||||
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
|
||||
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
|
||||
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
|
||||
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
|
||||
...systemGeneratedUserEncryptionKey
|
||||
});
|
||||
const encKeys = await generateUserSrpKeys(email, password, {
|
||||
publicKey: systemGeneratedUserEncryptionKey.publicKey,
|
||||
privateKey: serverGeneratedPrivateKey
|
||||
});
|
||||
// now reencrypt server generated key with user provided password
|
||||
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey: encKeys.protectedKey,
|
||||
protectedKeyIV: encKeys.protectedKeyIV,
|
||||
protectedKeyTag: encKeys.protectedKeyTag,
|
||||
publicKey: encKeys.publicKey,
|
||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||
iv: encKeys.encryptedPrivateKeyIV,
|
||||
tag: encKeys.encryptedPrivateKeyTag,
|
||||
salt: encKeys.salt,
|
||||
verifier: encKeys.verifier,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
} else {
|
||||
userEncKey = await userDAL.upsertUserEncryptionKey(
|
||||
us.id,
|
||||
{
|
||||
encryptionVersion: UserEncryption.V2,
|
||||
salt,
|
||||
verifier,
|
||||
publicKey,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
hashedPassword,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const updatedMembersips = await orgDAL.updateMembership(
|
||||
|
@ -34,6 +34,7 @@ export enum AuthMode {
|
||||
}
|
||||
|
||||
export enum ActorType { // would extend to AWS, Azure, ...
|
||||
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
|
||||
USER = "user", // userIdentity
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity",
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
export const createSerialNumber = () => {
|
||||
const randomBytes = crypto.randomBytes(32);
|
||||
const randomBytes = crypto.randomBytes(20);
|
||||
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
||||
return randomBytes.toString("hex");
|
||||
};
|
||||
|
@ -19,7 +19,13 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertStatus
|
||||
} from "../certificate/certificate-types";
|
||||
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
@ -41,6 +47,7 @@ import {
|
||||
TCreateCaDTO,
|
||||
TDeleteCaDTO,
|
||||
TGetCaCertDTO,
|
||||
TGetCaCertificateTemplatesDTO,
|
||||
TGetCaCertsDTO,
|
||||
TGetCaCsrDTO,
|
||||
TGetCaDTO,
|
||||
@ -64,7 +71,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
>;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById" | "find">;
|
||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
@ -108,6 +115,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notAfter,
|
||||
maxPathLength,
|
||||
keyAlgorithm,
|
||||
requireTemplateForIssuance,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
@ -170,7 +178,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
serialNumber
|
||||
})
|
||||
}),
|
||||
requireTemplateForIssuance
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -302,7 +311,15 @@ export const certificateAuthorityServiceFactory = ({
|
||||
* Update CA with id [caId].
|
||||
* Note: Used to enable/disable CA
|
||||
*/
|
||||
const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => {
|
||||
const updateCaById = async ({
|
||||
caId,
|
||||
status,
|
||||
requireTemplateForIssuance,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
@ -319,7 +336,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status });
|
||||
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status, requireTemplateForIssuance });
|
||||
|
||||
return updatedCa;
|
||||
};
|
||||
@ -751,6 +768,39 @@ export const certificateAuthorityServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return CA certificate object by ID
|
||||
*/
|
||||
const getCaCertById = async ({ caId, caCertId }: { caId: string; caCertId: string }) => {
|
||||
const caCert = await certificateAuthorityCertDAL.findOne({
|
||||
caId,
|
||||
id: caCertId
|
||||
});
|
||||
|
||||
if (!caCert) {
|
||||
throw new NotFoundError({ message: "CA certificate not found" });
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
|
||||
return caCertObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Issue certificate to be imported back in for intermediate CA
|
||||
*/
|
||||
@ -765,6 +815,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notAfter,
|
||||
maxPathLength
|
||||
}: TSignIntermediateDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
@ -839,7 +890,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const { caPrivateKey } = await getCaCredentials({
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
@ -848,6 +899,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
@ -867,7 +923,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
@ -1041,7 +1101,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
@ -1077,6 +1139,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
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.requireTemplateForIssuance && !certificateTemplate) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
}
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
|
||||
@ -1154,16 +1219,70 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
// handle key usages
|
||||
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
|
||||
if (keyUsages === undefined && !certificateTemplate) {
|
||||
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
|
||||
}
|
||||
|
||||
if (keyUsages === undefined && certificateTemplate) {
|
||||
selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[];
|
||||
}
|
||||
|
||||
if (keyUsages?.length && certificateTemplate) {
|
||||
const validKeyUsages = certificateTemplate.keyUsages || [];
|
||||
if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid key usage value based on template policy"
|
||||
});
|
||||
}
|
||||
selectedKeyUsages = keyUsages;
|
||||
}
|
||||
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
// handle extended key usages
|
||||
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? [];
|
||||
if (extendedKeyUsages === undefined && certificateTemplate) {
|
||||
selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[];
|
||||
}
|
||||
|
||||
if (extendedKeyUsages?.length && certificateTemplate) {
|
||||
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
|
||||
if (extendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid extended key usage value based on template policy"
|
||||
});
|
||||
}
|
||||
selectedExtendedKeyUsages = extendedKeyUsages;
|
||||
}
|
||||
|
||||
if (selectedExtendedKeyUsages.length) {
|
||||
extensions.push(
|
||||
new x509.ExtendedKeyUsageExtension(
|
||||
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
@ -1245,7 +1364,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
altNames,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -1294,6 +1415,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
* Note: CSR is generated externally and submitted to Infisical.
|
||||
*/
|
||||
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
||||
const appCfg = getConfig();
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
|
||||
@ -1307,7 +1429,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter
|
||||
notAfter,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
} = dto;
|
||||
|
||||
let collectionId = pkiCollectionId;
|
||||
@ -1347,6 +1471,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
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.requireTemplateForIssuance && !certificateTemplate) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
}
|
||||
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
@ -1415,7 +1542,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
||||
});
|
||||
|
||||
const { caPrivateKey } = await getCaCredentials({
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
@ -1423,13 +1550,115 @@ export const certificateAuthorityServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
// handle key usages
|
||||
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||
let csrKeyUsages: CertKeyUsage[] = [];
|
||||
if (csrKeyUsageExtension) {
|
||||
csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
|
||||
if (keyUsages === undefined && !certificateTemplate) {
|
||||
if (csrKeyUsageExtension) {
|
||||
selectedKeyUsages = csrKeyUsages;
|
||||
} else {
|
||||
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
|
||||
}
|
||||
}
|
||||
|
||||
if (keyUsages === undefined && certificateTemplate) {
|
||||
if (csrKeyUsageExtension) {
|
||||
const validKeyUsages = certificateTemplate.keyUsages || [];
|
||||
if (csrKeyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid key usage value based on template policy"
|
||||
});
|
||||
}
|
||||
selectedKeyUsages = csrKeyUsages;
|
||||
} else {
|
||||
selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[];
|
||||
}
|
||||
}
|
||||
|
||||
if (keyUsages?.length && certificateTemplate) {
|
||||
const validKeyUsages = certificateTemplate.keyUsages || [];
|
||||
if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid key usage value based on template policy"
|
||||
});
|
||||
}
|
||||
selectedKeyUsages = keyUsages;
|
||||
}
|
||||
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
// handle extended key usages
|
||||
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||
let csrExtendedKeyUsages: CertExtendedKeyUsage[] = [];
|
||||
if (csrExtendedKeyUsageExtension) {
|
||||
csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||
);
|
||||
}
|
||||
|
||||
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? [];
|
||||
if (extendedKeyUsages === undefined && !certificateTemplate && csrExtendedKeyUsageExtension) {
|
||||
selectedExtendedKeyUsages = csrExtendedKeyUsages;
|
||||
}
|
||||
|
||||
if (extendedKeyUsages === undefined && certificateTemplate) {
|
||||
if (csrExtendedKeyUsageExtension) {
|
||||
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
|
||||
if (csrExtendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid extended key usage value based on template policy"
|
||||
});
|
||||
}
|
||||
selectedExtendedKeyUsages = csrExtendedKeyUsages;
|
||||
} else {
|
||||
selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[];
|
||||
}
|
||||
}
|
||||
|
||||
if (extendedKeyUsages?.length && certificateTemplate) {
|
||||
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
|
||||
if (extendedKeyUsages.some((keyUsage) => !validExtendedKeyUsages.includes(keyUsage))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid extended key usage value based on template policy"
|
||||
});
|
||||
}
|
||||
selectedExtendedKeyUsages = extendedKeyUsages;
|
||||
}
|
||||
|
||||
if (selectedExtendedKeyUsages.length) {
|
||||
extensions.push(
|
||||
new x509.ExtendedKeyUsageExtension(
|
||||
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let altNamesFromCsr: string = "";
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
@ -1525,7 +1754,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
altNames: altNamesFromCsr || altNames,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -1568,6 +1799,40 @@ export const certificateAuthorityServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of certificate templates for CA with id [caId].
|
||||
*/
|
||||
const getCaCertificateTemplates = async ({
|
||||
caId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetCaCertificateTemplatesDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const certificateTemplates = await certificateTemplateDAL.find({ caId });
|
||||
|
||||
return {
|
||||
certificateTemplates,
|
||||
ca
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createCa,
|
||||
getCaById,
|
||||
@ -1577,9 +1842,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
renewCaCert,
|
||||
getCaCerts,
|
||||
getCaCert,
|
||||
getCaCertById,
|
||||
signIntermediate,
|
||||
importCertToCa,
|
||||
issueCertFromCa,
|
||||
signCertFromCa
|
||||
signCertFromCa,
|
||||
getCaCertificateTemplates
|
||||
};
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user