mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-31 15:32:32 +00:00
Compare commits
151 Commits
daniel/rab
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
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 | ||
|
54e6f4b607 | ||
|
a8fd83652d | ||
|
87a9a87dcd | ||
|
0b882ece8c | ||
|
e005e94165 | ||
|
0e07eaaa01 | ||
|
e10e313af3 | ||
|
e6c0bbb25b | ||
|
cf42279e5b | ||
|
e7191c2f71 | ||
|
372b6cbaea | ||
|
26add7bfd1 | ||
|
f3d207ab5c | ||
|
e1cd632546 | ||
|
655ee4f118 | ||
|
34a2452bf5 | ||
|
7846a81636 | ||
|
6bdf3455f5 | ||
|
556ae168dd | ||
|
7b19d2aa6a | ||
|
bda9bb3d61 | ||
|
4b66a9343c | ||
|
4930d7fc02 | ||
|
ad644db512 | ||
|
ffaf145317 | ||
|
17b0d0081d | ||
|
ecf177fecc | ||
|
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 |
@@ -72,3 +72,6 @@ PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
||||
WORKFLOW_SLACK_CLIENT_ID=
|
||||
WORKFLOW_SLACK_CLIENT_SECRET=
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -63,6 +63,7 @@ yarn-error.log*
|
||||
|
||||
# Editor specific
|
||||
.vscode/*
|
||||
.idea/*
|
||||
|
||||
frontend-build
|
||||
|
||||
|
148
backend/package-lock.json
generated
148
backend/package-lock.json
generated
@@ -34,6 +34,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",
|
||||
@@ -5981,6 +5983,78 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@slack/logger": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz",
|
||||
"integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==",
|
||||
"dependencies": {
|
||||
"@types/node": ">=18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
"npm": ">= 8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@slack/oauth": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.1.tgz",
|
||||
"integrity": "sha512-TuR9PI6bYKX6qHC7FQI4keMnhj45TNfSNQtTU3mtnHUX4XLM2dYLvRkUNADyiLTle2qu2rsOQtCIsZJw6H0sDA==",
|
||||
"dependencies": {
|
||||
"@slack/logger": "^4",
|
||||
"@slack/web-api": "^7.3.4",
|
||||
"@types/jsonwebtoken": "^9",
|
||||
"@types/node": ">=18",
|
||||
"jsonwebtoken": "^9",
|
||||
"lodash.isstring": "^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"npm": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@slack/types": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@slack/types/-/types-2.12.0.tgz",
|
||||
"integrity": "sha512-yFewzUomYZ2BYaGJidPuIgjoYj5wqPDmi7DLSaGIkf+rCi4YZ2Z3DaiYIbz7qb/PL2NmamWjCvB7e9ArI5HkKg==",
|
||||
"engines": {
|
||||
"node": ">= 12.13.0",
|
||||
"npm": ">= 6.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@slack/web-api": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.3.4.tgz",
|
||||
"integrity": "sha512-KwLK8dlz2lhr3NO7kbYQ7zgPTXPKrhq1JfQc0etJ0K8LSJhYYnf8GbVznvgDT/Uz1/pBXfFQnoXjrQIOKAdSuw==",
|
||||
"dependencies": {
|
||||
"@slack/logger": "^4.0.0",
|
||||
"@slack/types": "^2.9.0",
|
||||
"@types/node": ">=18.0.0",
|
||||
"@types/retry": "0.12.0",
|
||||
"axios": "^1.7.4",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
"is-electron": "2.2.2",
|
||||
"is-stream": "^2",
|
||||
"p-queue": "^6",
|
||||
"p-retry": "^4",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18",
|
||||
"npm": ">= 8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@slack/web-api/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz",
|
||||
@@ -7186,6 +7260,11 @@
|
||||
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||
},
|
||||
"node_modules/@types/safe-regex": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.6.tgz",
|
||||
@@ -10385,6 +10464,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -12178,6 +12262,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-electron": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz",
|
||||
"integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg=="
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -14131,6 +14220,14 @@
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/p-is-promise": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz",
|
||||
@@ -14169,6 +14266,38 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue/node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-throttle": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.1.0.tgz",
|
||||
@@ -14180,6 +14309,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
@@ -15530,6 +15670,14 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
|
@@ -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",
|
||||
|
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");
|
||||
}
|
||||
});
|
||||
}
|
@@ -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>>;
|
@@ -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>>;
|
@@ -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({
|
||||
|
@@ -122,6 +122,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
@@ -138,7 +142,7 @@ 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,
|
||||
|
@@ -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
|
||||
|
@@ -5,9 +5,13 @@ 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";
|
||||
|
||||
@@ -33,7 +37,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"
|
||||
@@ -56,6 +63,8 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
TUserDALFactory,
|
||||
"findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "find" | "findById"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
@@ -71,7 +80,9 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
accessApprovalPolicyApproverDAL,
|
||||
additionalPrivilegeDAL,
|
||||
smtpService,
|
||||
userDAL
|
||||
userDAL,
|
||||
kmsService,
|
||||
projectSlackConfigDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const createAccessApprovalRequest = async ({
|
||||
isTemporary,
|
||||
@@ -166,13 +177,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 +215,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
||||
approvalUrl
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequest
|
||||
});
|
||||
|
@@ -1,9 +1,9 @@
|
||||
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, stripUndefinedInWhere } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
|
||||
@@ -33,23 +33,44 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
orgId,
|
||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
||||
eventType,
|
||||
actor,
|
||||
userAgentType
|
||||
})
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
||||
|
||||
.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");
|
||||
|
||||
if (actor) {
|
||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]);
|
||||
}
|
||||
|
||||
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) => ({
|
||||
...AuditLogsSchema.parse(doc),
|
||||
project: {
|
||||
name: doc.projectName,
|
||||
slug: doc.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,7 +23,7 @@ export const auditLogServiceFactory = ({
|
||||
auditLogQueue,
|
||||
permissionService
|
||||
}: TAuditLogServiceFactoryDep) => {
|
||||
const listProjectAuditLogs = async ({
|
||||
const listAuditLogs = async ({
|
||||
userAgentType,
|
||||
eventType,
|
||||
offset,
|
||||
@@ -36,14 +37,33 @@ export const auditLogServiceFactory = ({
|
||||
projectId,
|
||||
auditLogActor
|
||||
}: TListProjectAuditLogDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
if (projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
} else {
|
||||
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.Member);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -52,8 +72,9 @@ export const auditLogServiceFactory = ({
|
||||
eventType,
|
||||
userAgentType,
|
||||
actor: auditLogActor,
|
||||
projectId
|
||||
...(projectId ? { projectId } : { orgId: actorOrgId })
|
||||
});
|
||||
|
||||
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
||||
...el,
|
||||
event: { type: logEventType, metadata: eventMetadata },
|
||||
@@ -76,6 +97,6 @@ export const auditLogServiceFactory = ({
|
||||
|
||||
return {
|
||||
createAuditLog,
|
||||
listProjectAuditLogs
|
||||
listAuditLogs
|
||||
};
|
||||
};
|
||||
|
@@ -6,14 +6,14 @@ import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
auditLogActor?: string;
|
||||
projectId: string;
|
||||
projectId?: string;
|
||||
eventType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
userAgentType?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} & TProjectPermission;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAuditLogDTO = {
|
||||
event: Event;
|
||||
@@ -140,6 +140,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 +170,14 @@ 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"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -1192,6 +1200,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 +1462,63 @@ 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;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -1547,6 +1620,7 @@ export type Event =
|
||||
| GetCaCrls
|
||||
| IssueCert
|
||||
| SignCert
|
||||
| GetCaCertificateTemplates
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
@@ -1576,4 +1650,11 @@ export type Event =
|
||||
| DeleteCertificateTemplate
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig;
|
||||
| GetCertificateTemplateEstConfig
|
||||
| AttemptCreateSlackIntegration
|
||||
| AttemptReinstallSlackIntegration
|
||||
| UpdateSlackIntegration
|
||||
| DeleteSlackIntegration
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig;
|
||||
|
@@ -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`,
|
||||
|
@@ -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" });
|
||||
@@ -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,
|
||||
|
@@ -363,7 +363,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."
|
||||
@@ -447,7 +452,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 +477,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.",
|
||||
@@ -1037,14 +1049,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 +1083,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 +1126,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 +1184,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 +1199,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,
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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,
|
||||
@@ -464,11 +473,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,
|
||||
@@ -488,6 +499,7 @@ export const registerRoutes = async (
|
||||
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({
|
||||
@@ -872,7 +888,8 @@ export const registerRoutes = async (
|
||||
smtpService,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
projectSlackConfigDAL
|
||||
});
|
||||
|
||||
const secretService = secretServiceFactory({
|
||||
@@ -922,7 +939,9 @@ export const registerRoutes = async (
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
smtpService,
|
||||
accessApprovalPolicyApproverDAL
|
||||
accessApprovalPolicyApproverDAL,
|
||||
projectSlackConfigDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const secretReplicationService = secretReplicationServiceFactory({
|
||||
@@ -1150,6 +1169,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 +1262,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);
|
||||
|
@@ -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,7 +9,9 @@ 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 { 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";
|
||||
@@ -62,6 +65,68 @@ 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({
|
||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
||||
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()
|
||||
}),
|
||||
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 auditLogs = await server.services.auditLog.listAuditLogs({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.query,
|
||||
endDate: req.query.endDate,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActor: req.query.actor,
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
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;
|
||||
}
|
||||
});
|
||||
};
|
@@ -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({
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { CertKeyAlgorithm } from "../certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types";
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||
@@ -38,6 +38,7 @@ export type TCreateCaDTO = {
|
||||
notAfter?: string;
|
||||
maxPathLength: number;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
requireTemplateForIssuance: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCaDTO = {
|
||||
@@ -47,6 +48,7 @@ export type TGetCaDTO = {
|
||||
export type TUpdateCaDTO = {
|
||||
caId: string;
|
||||
status?: CaStatus;
|
||||
requireTemplateForIssuance?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
@@ -95,6 +97,8 @@ export type TIssueCertFromCaDTO = {
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO =
|
||||
@@ -110,6 +114,8 @@ export type TSignCertFromCaDTO =
|
||||
ttl?: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
@@ -123,8 +129,14 @@ export type TSignCertFromCaDTO =
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TGetCaCertificateTemplatesDTO = {
|
||||
caId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDNParts = {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
|
@@ -7,7 +7,7 @@ const isValidDate = (dateString: string) => {
|
||||
|
||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||
|
||||
export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
export const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
export const validateAltNamesField = z
|
||||
.string()
|
||||
.trim()
|
||||
|
@@ -9,7 +9,9 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
||||
commonName: true,
|
||||
subjectAlternativeName: true,
|
||||
pkiCollectionId: true,
|
||||
ttl: true
|
||||
ttl: true,
|
||||
keyUsages: true,
|
||||
extendedKeyUsages: true
|
||||
}).merge(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
|
@@ -57,7 +57,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: TCreateCertTemplateDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) {
|
||||
@@ -86,7 +88,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -113,7 +117,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: TUpdateCertTemplateDTO) => {
|
||||
const certTemplate = await certificateTemplateDAL.getById(id);
|
||||
if (!certTemplate) {
|
||||
@@ -153,7 +159,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
name,
|
||||
ttl
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
|
||||
export type TCreateCertTemplateDTO = {
|
||||
caId: string;
|
||||
@@ -7,6 +8,8 @@ export type TCreateCertTemplateDTO = {
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateCertTemplateDTO = {
|
||||
@@ -17,6 +20,8 @@ export type TUpdateCertTemplateDTO = {
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertTemplateDTO = {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum CertStatus {
|
||||
@@ -12,6 +14,36 @@ export enum CertKeyAlgorithm {
|
||||
ECDSA_P384 = "EC_secp384r1"
|
||||
}
|
||||
|
||||
export enum CertKeyUsage {
|
||||
DIGITAL_SIGNATURE = "digitalSignature",
|
||||
KEY_ENCIPHERMENT = "keyEncipherment",
|
||||
NON_REPUDIATION = "nonRepudiation",
|
||||
DATA_ENCIPHERMENT = "dataEncipherment",
|
||||
KEY_AGREEMENT = "keyAgreement",
|
||||
KEY_CERT_SIGN = "keyCertSign",
|
||||
CRL_SIGN = "cRLSign",
|
||||
ENCIPHER_ONLY = "encipherOnly",
|
||||
DECIPHER_ONLY = "decipherOnly"
|
||||
}
|
||||
|
||||
export enum CertExtendedKeyUsage {
|
||||
CLIENT_AUTH = "clientAuth",
|
||||
SERVER_AUTH = "serverAuth",
|
||||
CODE_SIGNING = "codeSigning",
|
||||
EMAIL_PROTECTION = "emailProtection",
|
||||
TIMESTAMPING = "timeStamping",
|
||||
OCSP_SIGNING = "ocspSigning"
|
||||
}
|
||||
|
||||
export const CertExtendedKeyUsageOIDToName: Record<string, CertExtendedKeyUsage> = {
|
||||
[x509.ExtendedKeyUsage.clientAuth]: CertExtendedKeyUsage.CLIENT_AUTH,
|
||||
[x509.ExtendedKeyUsage.serverAuth]: CertExtendedKeyUsage.SERVER_AUTH,
|
||||
[x509.ExtendedKeyUsage.codeSigning]: CertExtendedKeyUsage.CODE_SIGNING,
|
||||
[x509.ExtendedKeyUsage.emailProtection]: CertExtendedKeyUsage.EMAIL_PROTECTION,
|
||||
[x509.ExtendedKeyUsage.ocspSigning]: CertExtendedKeyUsage.OCSP_SIGNING,
|
||||
[x509.ExtendedKeyUsage.timeStamping]: CertExtendedKeyUsage.TIMESTAMPING
|
||||
};
|
||||
|
||||
export enum CrlReason {
|
||||
UNSPECIFIED = "UNSPECIFIED",
|
||||
KEY_COMPROMISE = "KEY_COMPROMISE",
|
||||
|
@@ -95,6 +95,30 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByUserId = async (userId: string, orgId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.UserGroupMembership)
|
||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
.join(TableName.Groups, function () {
|
||||
this.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn(
|
||||
`${TableName.Groups}.orgId`,
|
||||
"=",
|
||||
db.raw("?", [orgId])
|
||||
);
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Groups),
|
||||
db.ref("name").withSchema(TableName.Groups),
|
||||
db.ref("slug").withSchema(TableName.Groups),
|
||||
db.ref("orgId").withSchema(TableName.Groups)
|
||||
);
|
||||
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByUserId" });
|
||||
}
|
||||
};
|
||||
|
||||
// The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId).
|
||||
// We need to join the GroupProjectMembership table with the Groups table to get the group name and slug.
|
||||
// We also need to join the GroupProjectMembershipRole table to get the role of the group in the project.
|
||||
@@ -197,5 +221,5 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
return members;
|
||||
};
|
||||
|
||||
return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers };
|
||||
return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers };
|
||||
};
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TIdentities } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types";
|
||||
|
||||
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
|
||||
|
||||
@@ -107,12 +109,45 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => {
|
||||
const findByProjectId = async (
|
||||
projectId: string,
|
||||
filter: { identityId?: string } & Pick<
|
||||
TListProjectIdentityDTO,
|
||||
"limit" | "offset" | "search" | "orderBy" | "orderDirection"
|
||||
> = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
|
||||
// TODO: scott - optimize, there's redundancy here with project membership and the below query
|
||||
const fetchIdentitySubquery = (tx || db.replicaNode())(TableName.Identity)
|
||||
.where((qb) => {
|
||||
if (filter.search) {
|
||||
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
|
||||
}
|
||||
})
|
||||
.join(
|
||||
TableName.IdentityProjectMembership,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.Identity}.id`
|
||||
)
|
||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||
.orderBy(
|
||||
`${TableName.Identity}.${filter.orderBy ?? ProjectIdentityOrderBy.Name}`,
|
||||
filter.orderDirection ?? OrderByDirection.ASC
|
||||
)
|
||||
.select(selectAllTableCols(TableName.Identity))
|
||||
.as(TableName.Identity); // required for subqueries
|
||||
|
||||
if (filter.limit) {
|
||||
void fetchIdentitySubquery.offset(filter.offset ?? 0).limit(filter.limit);
|
||||
}
|
||||
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityProjectMembership)
|
||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.join<TIdentities, TIdentities>(fetchIdentitySubquery, (bd) => {
|
||||
bd.on(`${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`);
|
||||
})
|
||||
.where((qb) => {
|
||||
if (filter.identityId) {
|
||||
void qb.where("identityId", filter.identityId);
|
||||
@@ -154,6 +189,19 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
||||
);
|
||||
|
||||
// TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point
|
||||
if (filter.orderBy) {
|
||||
switch (filter.orderBy) {
|
||||
case "name":
|
||||
void query.orderBy(`${TableName.Identity}.${filter.orderBy}`, filter.orderDirection);
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
|
||||
@@ -208,9 +256,37 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getCountByProjectId = async (
|
||||
projectId: string,
|
||||
filter: { identityId?: string } & Pick<TListProjectIdentityDTO, "search"> = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const identities = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
|
||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.where((qb) => {
|
||||
if (filter.identityId) {
|
||||
void qb.where("identityId", filter.identityId);
|
||||
}
|
||||
|
||||
if (filter.search) {
|
||||
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
|
||||
}
|
||||
})
|
||||
.count();
|
||||
|
||||
return Number(identities[0].count);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetCountByProjectId" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...identityProjectOrm,
|
||||
findByIdentityId,
|
||||
findByProjectId
|
||||
findByProjectId,
|
||||
getCountByProjectId
|
||||
};
|
||||
};
|
||||
|
@@ -268,7 +268,12 @@ export const identityProjectServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
}: TListProjectIdentityDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -279,8 +284,17 @@ export const identityProjectServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
|
||||
const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
|
||||
return identityMemberships;
|
||||
const identityMemberships = await identityProjectDAL.findByProjectId(projectId, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
});
|
||||
|
||||
const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { search });
|
||||
|
||||
return { identityMemberships, totalCount };
|
||||
};
|
||||
|
||||
const getProjectIdentityByIdentityId = async ({
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||
|
||||
@@ -40,8 +40,18 @@ export type TDeleteProjectIdentityDTO = {
|
||||
identityId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TListProjectIdentityDTO = TProjectPermission;
|
||||
export type TListProjectIdentityDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: ProjectIdentityOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectIdentityByIdentityIdDTO = {
|
||||
identityId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export enum ProjectIdentityOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
||||
|
||||
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
|
||||
|
||||
@@ -27,9 +29,20 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
|
||||
const find = async (
|
||||
{
|
||||
limit,
|
||||
offset = 0,
|
||||
orderBy,
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
search,
|
||||
...filter
|
||||
}: Partial<TIdentityOrgMemberships> &
|
||||
Pick<TListOrgIdentitiesByOrgIdDTO, "offset" | "limit" | "orderBy" | "orderDirection" | "search">,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
.where(filter)
|
||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
@@ -44,6 +57,30 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
||||
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
||||
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
|
||||
|
||||
if (limit) {
|
||||
void query.offset(offset).limit(limit);
|
||||
}
|
||||
|
||||
if (orderBy) {
|
||||
switch (orderBy) {
|
||||
case "name":
|
||||
void query.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection);
|
||||
break;
|
||||
case "role":
|
||||
void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, orderDirection);
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (search?.length) {
|
||||
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
return docs.map(
|
||||
({
|
||||
crId,
|
||||
@@ -79,5 +116,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...identityOrgOrm, find, findOne };
|
||||
const countAllOrgIdentities = async (
|
||||
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
.where(filter)
|
||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.count();
|
||||
|
||||
if (search?.length) {
|
||||
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
|
||||
}
|
||||
|
||||
const identities = await query;
|
||||
|
||||
return Number(identities[0].count);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "countAllOrgIdentities" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities };
|
||||
};
|
||||
|
@@ -6,7 +6,6 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
TCreateIdentityDTO,
|
||||
TDeleteIdentityDTO,
|
||||
TGetIdentityByIdDTO,
|
||||
TListOrgIdentitiesByOrgIdDTO,
|
||||
TListProjectIdentitiesByIdentityIdDTO,
|
||||
TUpdateIdentityDTO
|
||||
} from "./identity-types";
|
||||
@@ -195,14 +195,36 @@ export const identityServiceFactory = ({
|
||||
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
|
||||
};
|
||||
|
||||
const listOrgIdentities = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPermission) => {
|
||||
const listOrgIdentities = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
}: TListOrgIdentitiesByOrgIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityMemberships = await identityOrgMembershipDAL.find({
|
||||
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId
|
||||
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
});
|
||||
return identityMemberships;
|
||||
|
||||
const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({
|
||||
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
|
||||
search
|
||||
});
|
||||
|
||||
return { identityMemberships, totalCount };
|
||||
};
|
||||
|
||||
const listProjectIdentitiesByIdentityId = async ({
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { IPType } from "@app/lib/ip";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateIdentityDTO = {
|
||||
role: string;
|
||||
@@ -29,3 +29,16 @@ export interface TIdentityTrustedIp {
|
||||
export type TListProjectIdentitiesByIdentityIdDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TListOrgIdentitiesByOrgIdDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: OrgIdentityOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export enum OrgIdentityOrderBy {
|
||||
Name = "name",
|
||||
Role = "role"
|
||||
}
|
||||
|
@@ -207,6 +207,12 @@ const syncSecretsGCPSecretManager = async ({
|
||||
}
|
||||
);
|
||||
|
||||
if (!secrets[key].value) {
|
||||
logger.warn(
|
||||
`syncSecretsGcpsecretManager: create secret value in gcp where [key=${key}] and integration appId [appId=${integration.appId}]`
|
||||
);
|
||||
}
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
@@ -237,6 +243,12 @@ const syncSecretsGCPSecretManager = async ({
|
||||
}
|
||||
);
|
||||
} else if (secrets[key].value !== res[key]) {
|
||||
if (!secrets[key].value) {
|
||||
logger.warn(
|
||||
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and integration appId [appId=${integration.appId}]`
|
||||
);
|
||||
}
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
|
@@ -208,6 +208,23 @@ export const kmsServiceFactory = ({
|
||||
return org.kmsDefaultKeyId;
|
||||
};
|
||||
|
||||
const encryptWithRootKey = async () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return ({ plainText }: { plainText: Buffer }) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
|
||||
};
|
||||
};
|
||||
|
||||
const decryptWithRootKey = async () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
|
||||
return Promise.resolve(decryptedBlob);
|
||||
};
|
||||
};
|
||||
|
||||
const decryptWithKmsKey = async ({
|
||||
kmsId,
|
||||
depth = 0
|
||||
@@ -808,6 +825,8 @@ export const kmsServiceFactory = ({
|
||||
decryptWithKmsKey,
|
||||
encryptWithInputKey,
|
||||
decryptWithInputKey,
|
||||
encryptWithRootKey,
|
||||
decryptWithRootKey,
|
||||
getOrgKmsKeyId,
|
||||
getProjectSecretManagerKmsKeyId,
|
||||
updateProjectSecretManagerKmsKey,
|
||||
|
@@ -153,7 +153,6 @@ export const orgAdminServiceFactory = ({
|
||||
members: [
|
||||
{
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin,
|
||||
userPublicKey: userEncryptionKey.publicKey
|
||||
}
|
||||
]
|
||||
|
@@ -9,7 +9,10 @@ import {
|
||||
OrgMembershipStatus,
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TProjectMemberships,
|
||||
TProjectUserMembershipRolesInsert,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
@@ -18,13 +21,15 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
@@ -32,14 +37,14 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
|
||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { verifyProjectVersions } from "../project/project-fns";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||
@@ -84,6 +89,7 @@ type TOrgServiceFactoryDep = {
|
||||
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||
>;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
@@ -108,6 +114,7 @@ export const orgServiceFactory = ({
|
||||
tokenService,
|
||||
orgBotDAL,
|
||||
licenseService,
|
||||
projectRoleDAL,
|
||||
samlConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
@@ -440,18 +447,17 @@ export const orgServiceFactory = ({
|
||||
*/
|
||||
const inviteUserToOrganization = async ({
|
||||
orgId,
|
||||
userId,
|
||||
actorId,
|
||||
actor,
|
||||
inviteeEmails,
|
||||
organizationRoleSlug,
|
||||
projectRoleSlug,
|
||||
projectIds,
|
||||
projects: invitedProjects,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TInviteUserToOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
|
||||
@@ -461,6 +467,13 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
||||
if (isEmailInvalid) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
});
|
||||
}
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
||||
@@ -475,205 +488,333 @@ export const orgServiceFactory = ({
|
||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
|
||||
if (projectIds?.length) {
|
||||
const projects = await projectDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: projectIds
|
||||
}
|
||||
});
|
||||
|
||||
// if its not v3, throw an error
|
||||
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
|
||||
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
|
||||
if (isCustomOrgRole) {
|
||||
if (!plan?.rbac)
|
||||
throw new BadRequestError({
|
||||
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
||||
const users: Pick<
|
||||
TUsers & { orgId: string },
|
||||
"id" | "firstName" | "lastName" | "email" | "orgId" | "username"
|
||||
>[] = [];
|
||||
const projectsToInvite = invitedProjects?.length
|
||||
? await projectDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: invitedProjects?.map(({ id }) => id)
|
||||
}
|
||||
})
|
||||
: [];
|
||||
if (projectsToInvite.length !== invitedProjects?.length) {
|
||||
throw new UnauthorizedError({
|
||||
message: "One or more project doesn't have access to"
|
||||
});
|
||||
}
|
||||
|
||||
if (projectsToInvite.some((el) => el.version !== ProjectVersion.V3)) {
|
||||
throw new BadRequestError({
|
||||
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||
});
|
||||
}
|
||||
|
||||
const mailsForOrgInvitation: { email: string; userId: string; firstName: string; lastName: string }[] = [];
|
||||
const mailsForProjectInvitaion: { email: string[]; projectName: string }[] = [];
|
||||
const newProjectMemberships: TProjectMemberships[] = [];
|
||||
await orgDAL.transaction(async (tx) => {
|
||||
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
|
||||
|
||||
for await (const inviteeEmail of inviteeEmails) {
|
||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
let inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
// if the user doesn't exist we create the user with the email
|
||||
if (!inviteeUser) {
|
||||
inviteeUser = await userDAL.create(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
|
||||
isAccepted: false,
|
||||
email: inviteeEmail,
|
||||
username: inviteeEmail,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
{ tx }
|
||||
tx
|
||||
);
|
||||
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to invite members because ${inviteeEmail} is already part of the organization`,
|
||||
name: "Invite user to org"
|
||||
});
|
||||
}
|
||||
|
||||
if (!inviteeMembership) {
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (projectIds?.length) {
|
||||
if (
|
||||
organizationRoleSlug === OrgMembershipRole.Custom ||
|
||||
projectRoleSlug === ProjectMembershipRole.Custom
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Custom roles are not supported for inviting users to projects and organizations"
|
||||
});
|
||||
}
|
||||
|
||||
if (!projectRoleSlug) {
|
||||
throw new BadRequestError({
|
||||
message: "Selecting a project role is required to invite users to projects"
|
||||
});
|
||||
}
|
||||
|
||||
await projectMembershipDAL.insertMany(
|
||||
projectIds.map((id) => ({ projectId: id, userId: inviteeUser.id })),
|
||||
tx
|
||||
);
|
||||
for await (const projectId of projectIds) {
|
||||
await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject(
|
||||
{
|
||||
emails: [inviteeEmail],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
{
|
||||
tx
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [{ ...inviteeUser, orgId }];
|
||||
}
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
||||
if (isEmailInvalid) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
|
||||
const inviteeUserId = inviteeUser?.id;
|
||||
const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
|
||||
|
||||
// when user is missing the encrytion keys
|
||||
// this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp
|
||||
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
|
||||
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
|
||||
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
|
||||
const serverGeneratedPassword = crypto.randomBytes(32).toString("hex");
|
||||
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(serverGeneratedPassword);
|
||||
const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword);
|
||||
await userDAL.createUserEncryption(
|
||||
{
|
||||
userId: inviteeUserId,
|
||||
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,
|
||||
serverEncryptedPrivateKeyEncoding: encoding,
|
||||
serverEncryptedPrivateKeyTag: tag,
|
||||
serverEncryptedPrivateKeyIV: iv,
|
||||
serverEncryptedPrivateKey: ciphertext
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
// if there exist no org membership we set is as given by the request
|
||||
if (!inviteeMembership) {
|
||||
// as its used by project invite also
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
let roleId;
|
||||
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
|
||||
if (isCustomOrgRole) {
|
||||
const customRole = await orgRoleDAL.findOne({ slug: organizationRoleSlug, orgId });
|
||||
if (!customRole)
|
||||
throw new BadRequestError({ name: "Invite membership", message: "Organization role not found" });
|
||||
roleId = customRole.id;
|
||||
}
|
||||
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: orgRole,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true,
|
||||
roleId
|
||||
},
|
||||
tx
|
||||
);
|
||||
mailsForOrgInvitation.push({
|
||||
email: inviteeEmail,
|
||||
userId: inviteeUser.id,
|
||||
firstName: inviteeUser?.firstName || "",
|
||||
lastName: inviteeUser.lastName || ""
|
||||
});
|
||||
}
|
||||
// not invited before
|
||||
const user = await userDAL.create(
|
||||
{
|
||||
username: inviteeEmail,
|
||||
email: inviteeEmail,
|
||||
isAccepted: false,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: organizationRoleSlug,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
users.push({
|
||||
...user,
|
||||
orgId
|
||||
users.push(inviteeUser);
|
||||
}
|
||||
|
||||
const userIds = users.map(({ id }) => id);
|
||||
const usernames = users.map((el) => el.username);
|
||||
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
|
||||
// we don't need to spam with email. Thus org invitation doesn't need project invitation again
|
||||
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
|
||||
|
||||
// if there exist no project membership we set is as given by the request
|
||||
for await (const project of projectsToInvite) {
|
||||
const projectId = project.id;
|
||||
const { permission: projectPermission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(projectPermission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
const existingMembers = await projectMembershipDAL.find(
|
||||
{
|
||||
projectId: project.id,
|
||||
$in: { userId: userIds }
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId);
|
||||
const userIdsToExcludeAsPartOfGroup = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernames, projectId, tx)
|
||||
);
|
||||
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
|
||||
(user) => !existingMembersGroupByUserId?.[user.userId] && !userIdsToExcludeAsPartOfGroup.has(user.userId)
|
||||
);
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!userWithEncryptionKeyInvitedToProject.length) continue;
|
||||
|
||||
// validate custom project role
|
||||
const invitedProjectRoles = invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || [
|
||||
ProjectMembershipRole.Member
|
||||
];
|
||||
|
||||
const customProjectRoles = invitedProjectRoles.filter(
|
||||
(role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
||||
);
|
||||
const hasCustomRole = Boolean(customProjectRoles.length);
|
||||
if (hasCustomRole) {
|
||||
if (!plan?.rbac)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||
});
|
||||
}
|
||||
|
||||
const customRoles = hasCustomRole
|
||||
? await projectRoleDAL.find({
|
||||
projectId,
|
||||
$in: { slug: customProjectRoles.map((role) => role) }
|
||||
})
|
||||
: [];
|
||||
if (customRoles.length !== customProjectRoles.length)
|
||||
throw new BadRequestError({ message: "Custom role not found" });
|
||||
|
||||
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId }, tx);
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
|
||||
orgMembershipId: userEnc.userId,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin,
|
||||
userPublicKey: userEnc.publicKey
|
||||
}))
|
||||
});
|
||||
|
||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
|
||||
projectId,
|
||||
userId: userEnc.userId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
newProjectMemberships.push(...projectMemberships);
|
||||
|
||||
const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = [];
|
||||
invitedProjectRoles.forEach((projectRole) => {
|
||||
const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]);
|
||||
projectMemberships.forEach((membership) => {
|
||||
sanitizedProjectMembershipRoles.push({
|
||||
projectMembershipId: membership.id,
|
||||
role: isCustomRole ? ProjectMembershipRole.Custom : projectRole,
|
||||
customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null
|
||||
});
|
||||
});
|
||||
});
|
||||
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
|
||||
|
||||
await projectKeyDAL.insertMany(
|
||||
newWsMembers.map((el) => ({
|
||||
encryptedKey: el.workspaceEncryptedKey,
|
||||
nonce: el.workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: el.orgMembershipId,
|
||||
projectId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
mailsForProjectInvitaion.push({
|
||||
email: userWithEncryptionKeyInvitedToProject
|
||||
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
|
||||
.map((el) => el.email || el.username),
|
||||
projectName: project.name
|
||||
});
|
||||
}
|
||||
return users;
|
||||
});
|
||||
|
||||
const user = await userDAL.findById(userId);
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
const signupTokens: { email: string; link: string }[] = [];
|
||||
if (inviteeUsers) {
|
||||
for await (const invitee of inviteeUsers) {
|
||||
// send org invite mail
|
||||
await Promise.allSettled(
|
||||
mailsForOrgInvitation.map(async (el) => {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: invitee.id,
|
||||
userId: el.userId,
|
||||
orgId
|
||||
});
|
||||
|
||||
let inviteMetadata: string = "";
|
||||
if (projectIds && projectIds?.length > 0) {
|
||||
inviteMetadata = jwt.sign(
|
||||
{
|
||||
type: TokenMetadataType.InviteToProjects,
|
||||
payload: {
|
||||
projectIds,
|
||||
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
}
|
||||
} satisfies TTokenMetadata,
|
||||
appCfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn: appCfg.JWT_INVITE_LIFETIME
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
signupTokens.push({
|
||||
email: invitee.email || invitee.username,
|
||||
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${
|
||||
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
|
||||
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
|
||||
email: el.email,
|
||||
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${org?.id}`
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
return smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [invitee.email || invitee.username],
|
||||
recipients: [el.email],
|
||||
substitutions: {
|
||||
metadata: inviteMetadata,
|
||||
inviterFirstName: user.firstName,
|
||||
inviterUsername: user.username,
|
||||
inviterFirstName: el.firstName,
|
||||
inviterUsername: el.email,
|
||||
organizationName: org?.name,
|
||||
email: invitee.email || invitee.username,
|
||||
email: el.email,
|
||||
organizationId: org?.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.allSettled(
|
||||
mailsForProjectInvitaion
|
||||
.filter((el) => Boolean(el.email.length))
|
||||
.map(async (el) => {
|
||||
return smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical project invitation",
|
||||
recipients: el.email,
|
||||
substitutions: {
|
||||
workspaceName: el.projectName,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (!appCfg.isSmtpConfigured) {
|
||||
return signupTokens;
|
||||
return { signupTokens, projectMemberships: newProjectMemberships };
|
||||
}
|
||||
|
||||
return { signupTokens: undefined, projectMemberships: newProjectMemberships };
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@@ -26,14 +25,17 @@ export type TDeleteOrgMembershipDTO = {
|
||||
};
|
||||
|
||||
export type TInviteUserToOrgDTO = {
|
||||
userId: string;
|
||||
actorId: string;
|
||||
actor: ActorType;
|
||||
orgId: string;
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug: OrgMembershipRole;
|
||||
projectIds?: string[];
|
||||
projectRoleSlug?: ProjectMembershipRole;
|
||||
organizationRoleSlug: string;
|
||||
projects?: {
|
||||
id: string;
|
||||
projectRoleSlug?: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TVerifyUserToOrgDTO = {
|
||||
|
@@ -1,190 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
||||
|
||||
type TAddMembersToProjectArg = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectById" | "findProjectGhostUser">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "insertMany">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
type AddMembersToNonE2EEProjectDTO = {
|
||||
emails: string[];
|
||||
usernames: string[];
|
||||
projectId: string;
|
||||
projectMembershipRole: ProjectMembershipRole;
|
||||
sendEmails?: boolean;
|
||||
};
|
||||
|
||||
type AddMembersToNonE2EEProjectOptions = {
|
||||
tx?: Knex;
|
||||
throwOnProjectNotFound?: boolean;
|
||||
};
|
||||
|
||||
export const addMembersToProject = ({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}: TAddMembersToProjectArg) => {
|
||||
// Can create multiple memberships for a singular project, based on user email / username
|
||||
const addMembersToNonE2EEProject = async (
|
||||
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
||||
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
||||
) => {
|
||||
const processTransaction = async (tx: Knex) => {
|
||||
const usernamesAndEmails = [...emails, ...usernames];
|
||||
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
if (!project) {
|
||||
if (options.throwOnProjectNotFound) {
|
||||
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const orgMembers = await orgDAL.findOrgMembersByUsername(
|
||||
project.orgId,
|
||||
[...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))],
|
||||
tx
|
||||
);
|
||||
|
||||
if (orgMembers.length !== usernamesAndEmails.length)
|
||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
|
||||
if (!orgMembers.length) return [];
|
||||
|
||||
const existingMembers = await projectMembershipDAL.find({
|
||||
projectId,
|
||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||
});
|
||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: orgMembers.map((membership) => ({
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole,
|
||||
userPublicKey: membership.user.publicKey
|
||||
}))
|
||||
});
|
||||
|
||||
const members: TProjectMemberships[] = [];
|
||||
|
||||
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||
);
|
||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||
orgMembers.map(({ user }) => ({
|
||||
projectId,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.insertMany(
|
||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
||||
tx
|
||||
);
|
||||
|
||||
members.push(...projectMemberships);
|
||||
|
||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||
await projectKeyDAL.insertMany(
|
||||
orgMembers
|
||||
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
||||
.map(({ user, id }) => ({
|
||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.id,
|
||||
projectId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
if (sendEmails) {
|
||||
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (recipients.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical project invitation",
|
||||
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
if (options.tx) {
|
||||
return processTransaction(options.tx);
|
||||
}
|
||||
return projectMembershipDAL.transaction(processTransaction);
|
||||
};
|
||||
|
||||
return {
|
||||
addMembersToNonE2EEProject
|
||||
};
|
||||
};
|
@@ -22,11 +22,9 @@ import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import { addMembersToProject } from "./project-membership-fns";
|
||||
import {
|
||||
ProjectUserMembershipTemporaryMode,
|
||||
TAddUsersToWorkspaceDTO,
|
||||
TAddUsersToWorkspaceNonE2EEDTO,
|
||||
TDeleteProjectMembershipOldDTO,
|
||||
TDeleteProjectMembershipsDTO,
|
||||
TGetProjectMembershipByUsernameDTO,
|
||||
@@ -44,7 +42,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "findOne">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||
@@ -61,7 +59,6 @@ export const projectMembershipServiceFactory = ({
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService,
|
||||
projectRoleDAL,
|
||||
projectBotDAL,
|
||||
orgDAL,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
userDAL,
|
||||
@@ -214,52 +211,6 @@ export const projectMembershipServiceFactory = ({
|
||||
return orgMembers;
|
||||
};
|
||||
|
||||
const addUsersToProjectNonE2EE = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
emails,
|
||||
usernames,
|
||||
sendEmails = true
|
||||
}: TAddUsersToWorkspaceNonE2EEDTO) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
|
||||
const members = await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject({
|
||||
emails,
|
||||
usernames,
|
||||
projectId,
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
sendEmails
|
||||
});
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
const updateProjectMembership = async ({
|
||||
actorId,
|
||||
actor,
|
||||
@@ -530,7 +481,6 @@ export const projectMembershipServiceFactory = ({
|
||||
getProjectMemberships,
|
||||
getProjectMembershipByUsername,
|
||||
updateProjectMembership,
|
||||
addUsersToProjectNonE2EE,
|
||||
deleteProjectMemberships,
|
||||
deleteProjectMembership, // TODO: Remove this
|
||||
addUsersToProject,
|
||||
|
@@ -53,4 +53,5 @@ export type TAddUsersToWorkspaceNonE2EEDTO = {
|
||||
sendEmails?: boolean;
|
||||
emails: string[];
|
||||
usernames: string[];
|
||||
roleSlugs?: string[];
|
||||
} & TProjectPermission;
|
||||
|
@@ -16,7 +16,7 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
|
||||
privateKey: userPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
|
||||
const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => {
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
|
||||
plaintextProjectKey,
|
||||
userPublicKey,
|
||||
@@ -25,7 +25,6 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
|
||||
|
||||
return {
|
||||
orgMembershipId,
|
||||
projectRole: projectMembershipRole,
|
||||
workspaceEncryptedKey: inviteeCipherText,
|
||||
workspaceEncryptedNonce: inviteeNonce
|
||||
};
|
||||
|
@@ -300,8 +300,7 @@ export const projectQueueFactory = ({
|
||||
members: [
|
||||
{
|
||||
userPublicKey: user.publicKey,
|
||||
orgMembershipId: orgMembership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin
|
||||
orgMembershipId: orgMembership.id
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@@ -34,6 +34,8 @@ import { TProjectUserMembershipRoleDALFactory } from "../project-membership/proj
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||
@@ -43,6 +45,7 @@ import {
|
||||
TDeleteProjectDTO,
|
||||
TGetProjectDTO,
|
||||
TGetProjectKmsKey,
|
||||
TGetProjectSlackConfig,
|
||||
TListProjectAlertsDTO,
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
@@ -54,6 +57,7 @@ import {
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectKmsDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectSlackConfig,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
TUpgradeProjectDTO
|
||||
} from "./project-types";
|
||||
@@ -76,6 +80,8 @@ type TProjectServiceFactoryDep = {
|
||||
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
@@ -126,7 +132,9 @@ export const projectServiceFactory = ({
|
||||
pkiAlertDAL,
|
||||
keyStore,
|
||||
kmsService,
|
||||
projectBotDAL
|
||||
projectBotDAL,
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -269,8 +277,7 @@ export const projectServiceFactory = ({
|
||||
members: [
|
||||
{
|
||||
userPublicKey: user.publicKey,
|
||||
orgMembershipId: orgMembership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin
|
||||
orgMembershipId: orgMembership.id
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -284,7 +291,7 @@ export const projectServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{ projectMembershipId: userProjectMembership.id, role: projectAdmin.projectRole },
|
||||
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -909,6 +916,113 @@ export const projectServiceFactory = ({
|
||||
return { secretManagerKmsKey: kmsKey };
|
||||
};
|
||||
|
||||
const getProjectSlackConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TGetProjectSlackConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
|
||||
return projectSlackConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
};
|
||||
|
||||
const updateProjectSlackConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
}: TUpdateProjectSlackConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found"
|
||||
});
|
||||
}
|
||||
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(slackIntegrationId);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
if (slackIntegration.orgId !== project.orgId) {
|
||||
throw new BadRequestError({
|
||||
message: "Selected slack integration is not in the same organization"
|
||||
});
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.transaction(async (tx) => {
|
||||
const slackConfig = await projectSlackConfigDAL.findOne(
|
||||
{
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (slackConfig) {
|
||||
return projectSlackConfigDAL.updateById(
|
||||
slackConfig.id,
|
||||
{
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return projectSlackConfigDAL.create(
|
||||
{
|
||||
projectId,
|
||||
slackIntegrationId,
|
||||
isAccessRequestNotificationEnabled,
|
||||
accessRequestChannels,
|
||||
isSecretRequestNotificationEnabled,
|
||||
secretRequestChannels
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createProject,
|
||||
deleteProject,
|
||||
@@ -929,6 +1043,8 @@ export const projectServiceFactory = ({
|
||||
updateProjectKmsKey,
|
||||
getProjectKmsBackup,
|
||||
loadProjectKmsBackup,
|
||||
getProjectKmsKeys
|
||||
getProjectKmsKeys,
|
||||
getProjectSlackConfig,
|
||||
updateProjectSlackConfig
|
||||
};
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@@ -88,7 +88,6 @@ export type AddUserToWsDTO = {
|
||||
userPrivateKey: string;
|
||||
members: {
|
||||
orgMembershipId: string;
|
||||
projectMembershipRole: ProjectMembershipRole;
|
||||
userPublicKey: string;
|
||||
}[];
|
||||
};
|
||||
@@ -123,3 +122,13 @@ export type TLoadProjectKmsBackupDTO = {
|
||||
export type TGetProjectKmsKey = TProjectPermission;
|
||||
|
||||
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||
|
||||
export type TGetProjectSlackConfig = TProjectPermission;
|
||||
|
||||
export type TUpdateProjectSlackConfig = {
|
||||
slackIntegrationId: string;
|
||||
isAccessRequestNotificationEnabled: boolean;
|
||||
accessRequestChannels: string;
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
} & TProjectPermission;
|
||||
|
@@ -444,7 +444,9 @@ export const expandSecretReferencesFactory = ({
|
||||
depth: depth + 1
|
||||
});
|
||||
}
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
|
||||
if (referedValue) {
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
|
||||
}
|
||||
} else {
|
||||
const secretReferenceEnvironment = entities[0];
|
||||
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
|
||||
@@ -463,7 +465,9 @@ export const expandSecretReferencesFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
|
||||
if (referedValue) {
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
backend/src/services/slack/project-slack-config-dal.ts
Normal file
25
backend/src/services/slack/project-slack-config-dal.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectSlackConfigDALFactory = ReturnType<typeof projectSlackConfigDALFactory>;
|
||||
|
||||
export const projectSlackConfigDALFactory = (db: TDbClient) => {
|
||||
const projectSlackConfigOrm = ormify(db, TableName.ProjectSlackConfigs);
|
||||
|
||||
const getIntegrationDetailsByProject = (projectId: string, tx?: Knex) => {
|
||||
return (tx || db.replicaNode())(TableName.ProjectSlackConfigs)
|
||||
.join(
|
||||
TableName.SlackIntegrations,
|
||||
`${TableName.ProjectSlackConfigs}.slackIntegrationId`,
|
||||
`${TableName.SlackIntegrations}.id`
|
||||
)
|
||||
.where("projectId", "=", projectId)
|
||||
.select(selectAllTableCols(TableName.ProjectSlackConfigs), selectAllTableCols(TableName.SlackIntegrations))
|
||||
.first();
|
||||
};
|
||||
|
||||
return { ...projectSlackConfigOrm, getIntegrationDetailsByProject };
|
||||
};
|
16
backend/src/services/slack/slack-auth-validators.ts
Normal file
16
backend/src/services/slack/slack-auth-validators.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import z from "zod";
|
||||
|
||||
export const validateSlackChannelsField = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
.transform((data) => {
|
||||
if (data === "") return "";
|
||||
return data
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
})
|
||||
.refine((data) => data.split(",").length <= 20, {
|
||||
message: "You can only select up to 20 slack channels"
|
||||
});
|
177
backend/src/services/slack/slack-fns.ts
Normal file
177
backend/src/services/slack/slack-fns.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { WebClient } from "@slack/web-api";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
|
||||
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
|
||||
|
||||
export const fetchSlackChannels = async (botKey: string) => {
|
||||
const slackChannels: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[] = [];
|
||||
|
||||
const slackWebClient = new WebClient(botKey);
|
||||
let cursor;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await slackWebClient.conversations.list({
|
||||
cursor,
|
||||
limit: 1000,
|
||||
types: "public_channel,private_channel"
|
||||
});
|
||||
|
||||
response.channels?.forEach((channel) =>
|
||||
slackChannels.push({
|
||||
name: channel.name_normalized as string,
|
||||
id: channel.id as string
|
||||
})
|
||||
);
|
||||
|
||||
// Set the cursor for the next page
|
||||
cursor = response.response_metadata?.next_cursor;
|
||||
} while (cursor); // Continue while there is a cursor
|
||||
|
||||
return slackChannels;
|
||||
};
|
||||
|
||||
const buildSlackPayload = (notification: TSlackNotification) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
switch (notification.type) {
|
||||
case SlackTriggerFeature.SECRET_APPROVAL: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
|
||||
*Environment*: ${payload.environment}
|
||||
*Secret path*: ${payload.secretPath || "/"}
|
||||
|
||||
View the complete details <${appCfg.SITE_URL}/project/${payload.projectId}/approval?requestId=${
|
||||
payload.requestId
|
||||
}|here>.`;
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "Secret approval request",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: messageBody
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
};
|
||||
}
|
||||
case SlackTriggerFeature.ACCESS_REQUEST: {
|
||||
const { payload } = notification;
|
||||
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
|
||||
payload.isTemporary ? "temporary" : "permanent"
|
||||
} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
|
||||
|
||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
||||
|
||||
View the request and approve or deny it <${payload.approvalUrl}|here>.`;
|
||||
|
||||
const payloadBlocks = [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "New access approval request pending for review",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: messageBody
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
payloadMessage: messageBody,
|
||||
payloadBlocks
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw new BadRequestError({
|
||||
message: "Slack notification type not supported."
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const triggerSlackNotification = async ({
|
||||
projectId,
|
||||
notification,
|
||||
projectSlackConfigDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
notification: TSlackNotification;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id);
|
||||
|
||||
if (!slackIntegration) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetChannelIds: string[] = [];
|
||||
if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) {
|
||||
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
|
||||
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
|
||||
return;
|
||||
}
|
||||
} else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) {
|
||||
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
|
||||
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: project.orgId
|
||||
});
|
||||
|
||||
const botKey = orgDataKeyDecryptor({
|
||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||
}).toString("utf8");
|
||||
|
||||
const slackWebClient = new WebClient(botKey);
|
||||
|
||||
for await (const conversationId of targetChannelIds) {
|
||||
// we send both text and blocks for compatibility with barebone clients
|
||||
await slackWebClient.chat
|
||||
.postMessage({
|
||||
channel: conversationId,
|
||||
text: payloadMessage,
|
||||
blocks: payloadBlocks
|
||||
})
|
||||
.catch((err) => logger.error(err));
|
||||
}
|
||||
};
|
56
backend/src/services/slack/slack-integration-dal.ts
Normal file
56
backend/src/services/slack/slack-integration-dal.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TSlackIntegrations, TWorkflowIntegrations } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TSlackIntegrationDALFactory = ReturnType<typeof slackIntegrationDALFactory>;
|
||||
|
||||
export const slackIntegrationDALFactory = (db: TDbClient) => {
|
||||
const slackIntegrationOrm = ormify(db, TableName.SlackIntegrations);
|
||||
|
||||
const findByIdWithWorkflowIntegrationDetails = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.SlackIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.SlackIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SlackIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(`${TableName.WorkflowIntegrations}.id`, "=", id)
|
||||
.first();
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
const findWithWorkflowIntegrationDetails = async (
|
||||
filter: Partial<TSlackIntegrations> & Partial<TWorkflowIntegrations>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
return await (tx || db.replicaNode())(TableName.SlackIntegrations)
|
||||
.join(
|
||||
TableName.WorkflowIntegrations,
|
||||
`${TableName.SlackIntegrations}.id`,
|
||||
`${TableName.WorkflowIntegrations}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SlackIntegrations))
|
||||
.select(db.ref("orgId").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("description").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("integration").withSchema(TableName.WorkflowIntegrations))
|
||||
.select(db.ref("slug").withSchema(TableName.WorkflowIntegrations))
|
||||
.where(filter);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find with Workflow integration details" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...slackIntegrationOrm, findByIdWithWorkflowIntegrationDetails, findWithWorkflowIntegrationDetails };
|
||||
};
|
463
backend/src/services/slack/slack-service.ts
Normal file
463
backend/src/services/slack/slack-service.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { InstallProvider } from "@slack/oauth";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { getServerCfg } from "../super-admin/super-admin-service";
|
||||
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
|
||||
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
|
||||
import { fetchSlackChannels } from "./slack-fns";
|
||||
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
|
||||
import {
|
||||
TCompleteSlackIntegrationDTO,
|
||||
TDeleteSlackIntegrationDTO,
|
||||
TGetReinstallUrlDTO,
|
||||
TGetSlackInstallUrlDTO,
|
||||
TGetSlackIntegrationByIdDTO,
|
||||
TGetSlackIntegrationByOrgDTO,
|
||||
TGetSlackIntegrationChannelsDTO,
|
||||
TReinstallSlackIntegrationDTO,
|
||||
TUpdateSlackIntegrationDTO
|
||||
} from "./slack-types";
|
||||
|
||||
type TSlackServiceFactoryDep = {
|
||||
slackIntegrationDAL: Pick<
|
||||
TSlackIntegrationDALFactory,
|
||||
| "deleteById"
|
||||
| "updateById"
|
||||
| "create"
|
||||
| "findByIdWithWorkflowIntegrationDetails"
|
||||
| "findWithWorkflowIntegrationDetails"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithRootKey" | "decryptWithRootKey">;
|
||||
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "transaction" | "create" | "updateById" | "deleteById">;
|
||||
};
|
||||
|
||||
export type TSlackServiceFactory = ReturnType<typeof slackServiceFactory>;
|
||||
|
||||
export const slackServiceFactory = ({
|
||||
permissionService,
|
||||
slackIntegrationDAL,
|
||||
kmsService,
|
||||
workflowIntegrationDAL
|
||||
}: TSlackServiceFactoryDep) => {
|
||||
const completeSlackIntegration = async ({
|
||||
orgId,
|
||||
slug,
|
||||
description,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
botAccessToken,
|
||||
slackBotId,
|
||||
slackBotUserId
|
||||
}: TCompleteSlackIntegrationDTO) => {
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(botAccessToken, "utf8")
|
||||
});
|
||||
|
||||
await workflowIntegrationDAL.transaction(async (tx) => {
|
||||
const workflowIntegration = await workflowIntegrationDAL.create(
|
||||
{
|
||||
description,
|
||||
orgId,
|
||||
slug,
|
||||
integration: WorkflowIntegration.SLACK
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await slackIntegrationDAL.create(
|
||||
{
|
||||
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
|
||||
id: workflowIntegration.id,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const reinstallSlackIntegration = async ({
|
||||
id,
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
botAccessToken,
|
||||
slackBotId,
|
||||
slackBotUserId
|
||||
}: TReinstallSlackIntegrationDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId: slackIntegration.orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBotAccessToken } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(botAccessToken, "utf8")
|
||||
});
|
||||
|
||||
await slackIntegrationDAL.updateById(id, {
|
||||
teamId,
|
||||
teamName,
|
||||
slackUserId,
|
||||
slackAppId,
|
||||
slackBotId,
|
||||
slackBotUserId,
|
||||
encryptedBotAccessToken
|
||||
});
|
||||
};
|
||||
|
||||
const getSlackInstaller = async () => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
||||
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
||||
|
||||
const decrypt = await kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
slackClientSecret = (
|
||||
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
|
||||
).toString();
|
||||
}
|
||||
|
||||
if (!slackClientId || !slackClientSecret) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid Slack configuration. ${
|
||||
appCfg.isCloud
|
||||
? "Please contact the Infisical team."
|
||||
: "Contact your instance admin to setup Slack integration in the Admin settings. Your configuration is missing Slack client ID and secret."
|
||||
}`
|
||||
});
|
||||
}
|
||||
|
||||
return new InstallProvider({
|
||||
clientId: slackClientId,
|
||||
clientSecret: slackClientSecret,
|
||||
stateSecret: appCfg.AUTH_SECRET,
|
||||
legacyStateVerification: true,
|
||||
installationStore: {
|
||||
storeInstallation: async (installation) => {
|
||||
if (installation.isEnterpriseInstall && installation.enterprise?.id) {
|
||||
throw new BadRequestError({
|
||||
message: "Enterprise not yet supported"
|
||||
});
|
||||
}
|
||||
|
||||
const metadata = JSON.parse(installation.metadata || "") as {
|
||||
id?: string;
|
||||
orgId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
if (metadata.id) {
|
||||
return reinstallSlackIntegration({
|
||||
id: metadata.id,
|
||||
teamId: installation.team?.id || "",
|
||||
teamName: installation.team?.name || "",
|
||||
slackUserId: installation.user.id,
|
||||
slackAppId: installation.appId || "",
|
||||
botAccessToken: installation.bot?.token || "",
|
||||
slackBotId: installation.bot?.id || "",
|
||||
slackBotUserId: installation.bot?.userId || ""
|
||||
});
|
||||
}
|
||||
|
||||
return completeSlackIntegration({
|
||||
orgId: metadata.orgId,
|
||||
slug: metadata.slug,
|
||||
description: metadata.description,
|
||||
teamId: installation.team?.id || "",
|
||||
teamName: installation.team?.name || "",
|
||||
slackUserId: installation.user.id,
|
||||
slackAppId: installation.appId || "",
|
||||
botAccessToken: installation.bot?.token || "",
|
||||
slackBotId: installation.bot?.id || "",
|
||||
slackBotUserId: installation.bot?.userId || ""
|
||||
});
|
||||
},
|
||||
// for our use-case we don't need to implement this because this will only be used
|
||||
// when listening for events from slack
|
||||
fetchInstallation: () => {
|
||||
return {} as never;
|
||||
},
|
||||
// for our use-case we don't need to implement this yet
|
||||
deleteInstallation: () => {
|
||||
return {} as never;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getInstallUrl = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug,
|
||||
description
|
||||
}: TGetSlackInstallUrlDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const installer = await getSlackInstaller();
|
||||
const url = await installer.generateInstallUrl({
|
||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||
metadata: JSON.stringify({
|
||||
slug,
|
||||
description,
|
||||
orgId: actorOrgId
|
||||
}),
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||
});
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const getReinstallUrl = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetReinstallUrlDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const installer = await getSlackInstaller();
|
||||
const url = await installer.generateInstallUrl({
|
||||
scopes: ["chat:write.public", "chat:write", "channels:read", "groups:read"],
|
||||
metadata: JSON.stringify({
|
||||
id,
|
||||
orgId: slackIntegration.orgId
|
||||
}),
|
||||
redirectUri: `${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`
|
||||
});
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const getSlackIntegrationsByOrg = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TGetSlackIntegrationByOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const slackIntegrations = await slackIntegrationDAL.findWithWorkflowIntegrationDetails({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return slackIntegrations;
|
||||
};
|
||||
|
||||
const getSlackIntegrationById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TGetSlackIntegrationByIdDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
return slackIntegration;
|
||||
};
|
||||
|
||||
const getSlackIntegrationChannels = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TGetSlackIntegrationChannelsDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
orgId: slackIntegration.orgId,
|
||||
type: KmsDataKey.Organization
|
||||
});
|
||||
|
||||
const botKey = orgDataKeyDecryptor({
|
||||
cipherTextBlob: slackIntegration.encryptedBotAccessToken
|
||||
}).toString("utf8");
|
||||
|
||||
return fetchSlackChannels(botKey);
|
||||
};
|
||||
|
||||
const updateSlackIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id,
|
||||
slug,
|
||||
description
|
||||
}: TUpdateSlackIntegrationDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
return workflowIntegrationDAL.transaction(async (tx) => {
|
||||
await workflowIntegrationDAL.updateById(
|
||||
slackIntegration.id,
|
||||
{
|
||||
slug,
|
||||
description
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const updatedIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(
|
||||
slackIntegration.id,
|
||||
tx
|
||||
);
|
||||
|
||||
return updatedIntegration!;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSlackIntegration = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TDeleteSlackIntegrationDTO) => {
|
||||
const slackIntegration = await slackIntegrationDAL.findByIdWithWorkflowIntegrationDetails(id);
|
||||
if (!slackIntegration) {
|
||||
throw new NotFoundError({
|
||||
message: "Slack integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
slackIntegration.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
await workflowIntegrationDAL.deleteById(id);
|
||||
|
||||
return slackIntegration;
|
||||
};
|
||||
|
||||
return {
|
||||
getInstallUrl,
|
||||
getReinstallUrl,
|
||||
getSlackIntegrationsByOrg,
|
||||
getSlackIntegrationById,
|
||||
completeSlackIntegration,
|
||||
getSlackInstaller,
|
||||
updateSlackIntegration,
|
||||
deleteSlackIntegration,
|
||||
getSlackIntegrationChannels
|
||||
};
|
||||
};
|
79
backend/src/services/slack/slack-types.ts
Normal file
79
backend/src/services/slack/slack-types.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetSlackInstallUrlDTO = {
|
||||
slug: string;
|
||||
description?: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetReinstallUrlDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetSlackIntegrationChannelsDTO = { id: string } & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
|
||||
TOrgPermission,
|
||||
"orgId"
|
||||
>;
|
||||
|
||||
export type TDeleteSlackIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TCompleteSlackIntegrationDTO = {
|
||||
orgId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
slackUserId: string;
|
||||
slackAppId: string;
|
||||
botAccessToken: string;
|
||||
slackBotId: string;
|
||||
slackBotUserId: string;
|
||||
};
|
||||
|
||||
export type TReinstallSlackIntegrationDTO = {
|
||||
id: string;
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
slackUserId: string;
|
||||
slackAppId: string;
|
||||
botAccessToken: string;
|
||||
slackBotId: string;
|
||||
slackBotUserId: string;
|
||||
};
|
||||
|
||||
export enum SlackTriggerFeature {
|
||||
SECRET_APPROVAL = "secret-approval",
|
||||
ACCESS_REQUEST = "access-request"
|
||||
}
|
||||
|
||||
export type TSlackNotification =
|
||||
| {
|
||||
type: SlackTriggerFeature.SECRET_APPROVAL;
|
||||
payload: {
|
||||
userEmail: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requestId: string;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SlackTriggerFeature.ACCESS_REQUEST;
|
||||
payload: {
|
||||
requesterFullName: string;
|
||||
requesterEmail: string;
|
||||
isTemporary: boolean;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectName: string;
|
||||
permissions: string[];
|
||||
approvalUrl: string;
|
||||
};
|
||||
};
|
@@ -6,10 +6,11 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||
import { AuthMethod } from "../auth/auth-type";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||
@@ -19,6 +20,7 @@ type TSuperAdminServiceFactoryDep = {
|
||||
serverCfgDAL: TSuperAdminDALFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||
@@ -39,6 +41,7 @@ export const superAdminServiceFactory = ({
|
||||
authService,
|
||||
orgService,
|
||||
keyStore,
|
||||
kmsService,
|
||||
licenseService
|
||||
}: TSuperAdminServiceFactoryDep) => {
|
||||
const initServerCfg = async () => {
|
||||
@@ -82,7 +85,12 @@ export const superAdminServiceFactory = ({
|
||||
return newCfg;
|
||||
};
|
||||
|
||||
const updateServerCfg = async (data: TSuperAdminUpdate, userId: string) => {
|
||||
const updateServerCfg = async (
|
||||
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string },
|
||||
userId: string
|
||||
) => {
|
||||
const updatedData = data;
|
||||
|
||||
if (data.enabledLoginMethods) {
|
||||
const superAdminUser = await userDAL.findById(userId);
|
||||
const loginMethodToAuthMethod = {
|
||||
@@ -113,7 +121,27 @@ export const superAdminServiceFactory = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);
|
||||
|
||||
const encryptWithRoot = await kmsService.encryptWithRootKey();
|
||||
if (data.slackClientId) {
|
||||
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({
|
||||
plainText: Buffer.from(data.slackClientId)
|
||||
});
|
||||
|
||||
updatedData.encryptedSlackClientId = encryptedClientId;
|
||||
updatedData.slackClientId = undefined;
|
||||
}
|
||||
|
||||
if (data.slackClientSecret) {
|
||||
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({
|
||||
plainText: Buffer.from(data.slackClientSecret)
|
||||
});
|
||||
|
||||
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
|
||||
updatedData.slackClientSecret = undefined;
|
||||
}
|
||||
|
||||
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
|
||||
|
||||
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
|
||||
|
||||
@@ -232,11 +260,38 @@ export const superAdminServiceFactory = ({
|
||||
return user;
|
||||
};
|
||||
|
||||
const getAdminSlackConfig = async () => {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
|
||||
if (!serverCfg) {
|
||||
throw new NotFoundError({ name: "Admin config", message: "Admin config not found" });
|
||||
}
|
||||
|
||||
let clientId = "";
|
||||
let clientSecret = "";
|
||||
|
||||
const decrypt = await kmsService.decryptWithRootKey();
|
||||
|
||||
if (serverCfg.encryptedSlackClientId) {
|
||||
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString();
|
||||
}
|
||||
|
||||
if (serverCfg.encryptedSlackClientSecret) {
|
||||
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
clientId,
|
||||
clientSecret
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
updateServerCfg,
|
||||
adminSignUp,
|
||||
getUsers,
|
||||
deleteUser
|
||||
deleteUser,
|
||||
getAdminSlackConfig
|
||||
};
|
||||
};
|
||||
|
@@ -82,10 +82,9 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findUserEncKeyByUserId = async (userId: string) => {
|
||||
const findUserEncKeyByUserId = async (userId: string, tx?: Knex) => {
|
||||
try {
|
||||
const user = await db
|
||||
.replicaNode()(TableName.Users)
|
||||
const user = await (tx || db.replicaNode())(TableName.Users)
|
||||
.where(`${TableName.Users}.id`, userId)
|
||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||
.first();
|
||||
|
@@ -4,18 +4,14 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
|
||||
let attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
|
||||
let attempt: string;
|
||||
let user;
|
||||
|
||||
let user = await userDAL.findOne({ username: attempt });
|
||||
if (!user) return attempt;
|
||||
|
||||
while (true) {
|
||||
do {
|
||||
attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
user = await userDAL.findOne({ username: attempt });
|
||||
} while (user);
|
||||
|
||||
if (!user) {
|
||||
return attempt;
|
||||
}
|
||||
}
|
||||
return attempt;
|
||||
};
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
@@ -8,8 +12,10 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
|
||||
import { AuthMethod } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TUserDALFactory } from "./user-dal";
|
||||
import { TListUserGroupsDTO } from "./user-types";
|
||||
|
||||
type TUserServiceFactoryDep = {
|
||||
userDAL: Pick<
|
||||
@@ -27,10 +33,12 @@ type TUserServiceFactoryDep = {
|
||||
| "delete"
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||
@@ -40,8 +48,10 @@ export const userServiceFactory = ({
|
||||
userAliasDAL,
|
||||
orgMembershipDAL,
|
||||
projectMembershipDAL,
|
||||
groupProjectDAL,
|
||||
tokenService,
|
||||
smtpService
|
||||
smtpService,
|
||||
permissionService
|
||||
}: TUserServiceFactoryDep) => {
|
||||
const sendEmailVerificationCode = async (username: string) => {
|
||||
const user = await userDAL.findOne({ username });
|
||||
@@ -295,6 +305,27 @@ export const userServiceFactory = ({
|
||||
return updatedOrgMembership.projectFavorites;
|
||||
};
|
||||
|
||||
const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => {
|
||||
const user = await userDAL.findOne({
|
||||
username
|
||||
});
|
||||
|
||||
// This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission.
|
||||
if (user.id !== actorId) {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
}
|
||||
|
||||
const memberships = await groupProjectDAL.findByUserId(user.id, actorOrgId);
|
||||
return memberships;
|
||||
};
|
||||
|
||||
return {
|
||||
sendEmailVerificationCode,
|
||||
verifyEmailVerificationCode,
|
||||
@@ -304,6 +335,7 @@ export const userServiceFactory = ({
|
||||
deleteUser,
|
||||
getMe,
|
||||
createUserAction,
|
||||
listUserGroups,
|
||||
getUserAction,
|
||||
unlockUser,
|
||||
getUserPrivateKey,
|
||||
|
@@ -0,0 +1,10 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TListUserGroupsDTO = {
|
||||
username: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export enum UserEncryption {
|
||||
V1 = 1,
|
||||
V2 = 2
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TWorkflowIntegrationDALFactory = ReturnType<typeof workflowIntegrationDALFactory>;
|
||||
|
||||
export const workflowIntegrationDALFactory = (db: TDbClient) => {
|
||||
const workflowIntegrationOrm = ormify(db, TableName.WorkflowIntegrations);
|
||||
|
||||
return workflowIntegrationOrm;
|
||||
};
|
@@ -0,0 +1,43 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
|
||||
import { TWorkflowIntegrationDALFactory } from "./workflow-integration-dal";
|
||||
import { TGetWorkflowIntegrationsByOrg } from "./workflow-integration-types";
|
||||
|
||||
type TWorkflowIntegrationServiceFactoryDep = {
|
||||
workflowIntegrationDAL: Pick<TWorkflowIntegrationDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
};
|
||||
|
||||
export type TWorkflowIntegrationServiceFactory = ReturnType<typeof workflowIntegrationServiceFactory>;
|
||||
|
||||
export const workflowIntegrationServiceFactory = ({
|
||||
workflowIntegrationDAL,
|
||||
permissionService
|
||||
}: TWorkflowIntegrationServiceFactoryDep) => {
|
||||
const getIntegrationsByOrg = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TGetWorkflowIntegrationsByOrg) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
return workflowIntegrationDAL.find({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
};
|
||||
return {
|
||||
getIntegrationsByOrg
|
||||
};
|
||||
};
|
@@ -0,0 +1,7 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export enum WorkflowIntegration {
|
||||
SLACK = "slack"
|
||||
}
|
||||
|
||||
export type TGetWorkflowIntegrationsByOrg = Omit<TOrgPermission, "orgId">;
|
@@ -4,22 +4,27 @@ Copyright (c) 2023 Infisical Inc.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/fatih/color"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ErrManualSignalInterrupt = errors.New("signal: interrupt")
|
||||
var watcherWaitGroup = new(sync.WaitGroup)
|
||||
|
||||
// runCmd represents the run command
|
||||
var runCmd = &cobra.Command{
|
||||
Example: `
|
||||
@@ -77,11 +82,35 @@ var runCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
command, err := cmd.Flags().GetString("command")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
watchMode, err := cmd.Flags().GetBool("watch")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
watchModeInterval, err := cmd.Flags().GetInt("watch-interval")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
// If the --watch flag has been set, the --watch-interval flag should also be set
|
||||
if watchMode && watchModeInterval < 5 {
|
||||
util.HandleError(fmt.Errorf("watch interval must be at least 5 seconds, you passed %d seconds", watchModeInterval))
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
@@ -116,108 +145,50 @@ var runCmd = &cobra.Command{
|
||||
Recursive: recursive,
|
||||
}
|
||||
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
request.InfisicalToken = token.Token
|
||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||
request.UniversalAuthAccessToken = token.Token
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir)
|
||||
|
||||
injectableEnvironment, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, shouldExpandSecrets, token)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||
}
|
||||
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
|
||||
log.Debug().Msgf("injecting the following environment variables into shell: %v", injectableEnvironment.Variables)
|
||||
|
||||
if watchMode {
|
||||
executeCommandWithWatchMode(command, args, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token)
|
||||
} else {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
|
||||
}
|
||||
if cmd.Flags().Changed("command") {
|
||||
command := cmd.Flag("command").Value.String()
|
||||
err = executeMultipleCommandWithEnvs(command, injectableEnvironment.SecretsCount, injectableEnvironment.Variables)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if shouldExpandSecrets {
|
||||
|
||||
authParams := models.ExpandSecretsAuthentication{}
|
||||
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
authParams.InfisicalToken = token.Token
|
||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||
authParams.UniversalAuthAccessToken = token.Token
|
||||
}
|
||||
|
||||
secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
|
||||
}
|
||||
|
||||
secretsByKey := getSecretsByKeys(secrets)
|
||||
environmentVariables := make(map[string]string)
|
||||
|
||||
// add all existing environment vars
|
||||
for _, s := range os.Environ() {
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
key := kv[0]
|
||||
value := kv[1]
|
||||
environmentVariables[key] = value
|
||||
}
|
||||
|
||||
// check to see if there are any reserved key words in secrets to inject
|
||||
filterReservedEnvVars(secretsByKey)
|
||||
|
||||
// now add infisical secrets
|
||||
for k, v := range secretsByKey {
|
||||
environmentVariables[k] = v.Value
|
||||
}
|
||||
|
||||
// turn it back into a list of envs
|
||||
var env []string
|
||||
for key, value := range environmentVariables {
|
||||
s := key + "=" + value
|
||||
env = append(env, s)
|
||||
}
|
||||
|
||||
log.Debug().Msgf("injecting the following environment variables into shell: %v", env)
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:run",
|
||||
posthog.NewProperties().
|
||||
Set("secretsCount", len(secrets)).
|
||||
Set("environment", environmentName).
|
||||
Set("isUsingServiceToken", token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER).
|
||||
Set("isUsingUniversalAuthToken", token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER).
|
||||
Set("single-command", strings.Join(args, " ")).
|
||||
Set("multi-command", cmd.Flag("command").Value.String()).
|
||||
Set("version", util.CLI_VERSION))
|
||||
|
||||
if cmd.Flags().Changed("command") {
|
||||
command := cmd.Flag("command").Value.String()
|
||||
|
||||
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
} else {
|
||||
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
err = executeSingleCommandWithEnvs(args, injectableEnvironment.SecretsCount, injectableEnvironment.Variables)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
reservedEnvVars = []string{
|
||||
"HOME", "PATH", "PS1", "PS2",
|
||||
"PWD", "EDITOR", "XAUTHORITY", "USER",
|
||||
"TERM", "TERMINFO", "SHELL", "MAIL",
|
||||
}
|
||||
|
||||
reservedEnvVarPrefixes = []string{
|
||||
"XDG_",
|
||||
"LC_",
|
||||
}
|
||||
)
|
||||
|
||||
func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) {
|
||||
var (
|
||||
reservedEnvVars = []string{
|
||||
"HOME", "PATH", "PS1", "PS2",
|
||||
"PWD", "EDITOR", "XAUTHORITY", "USER",
|
||||
"TERM", "TERMINFO", "SHELL", "MAIL",
|
||||
}
|
||||
|
||||
reservedEnvVarPrefixes = []string{
|
||||
"XDG_",
|
||||
"LC_",
|
||||
}
|
||||
)
|
||||
|
||||
for _, reservedEnvName := range reservedEnvVars {
|
||||
if _, ok := env[reservedEnvName]; ok {
|
||||
delete(env, reservedEnvName)
|
||||
@@ -237,13 +208,15 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) {
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
runCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
|
||||
runCmd.Flags().String("token", "", "fetch secrets using service token or machine identity access token")
|
||||
runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth")
|
||||
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
|
||||
runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
|
||||
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
runCmd.Flags().StringP("env", "e", "dev", "set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||
runCmd.Flags().Bool("expand", true, "parse shell parameter expansions in your secrets")
|
||||
runCmd.Flags().Bool("include-imports", true, "import linked secrets ")
|
||||
runCmd.Flags().Bool("recursive", false, "fetch secrets from all sub-folders")
|
||||
runCmd.Flags().Bool("secret-overriding", true, "prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
runCmd.Flags().Bool("watch", false, "enable reload of application when secrets change")
|
||||
runCmd.Flags().Int("watch-interval", 10, "interval in seconds to check for secret changes")
|
||||
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
||||
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
|
||||
runCmd.Flags().String("path", "/", "get secrets within a folder path")
|
||||
@@ -263,7 +236,7 @@ func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = env
|
||||
|
||||
return execCmd(cmd)
|
||||
return execBasicCmd(cmd)
|
||||
}
|
||||
|
||||
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
|
||||
@@ -286,11 +259,10 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []
|
||||
log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount))
|
||||
log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
|
||||
|
||||
return execCmd(cmd)
|
||||
return execBasicCmd(cmd)
|
||||
}
|
||||
|
||||
// Credit: inspired by AWS Valut
|
||||
func execCmd(cmd *exec.Cmd) error {
|
||||
func execBasicCmd(cmd *exec.Cmd) error {
|
||||
sigChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChannel)
|
||||
|
||||
@@ -314,3 +286,217 @@ func execCmd(cmd *exec.Cmd) error {
|
||||
os.Exit(waitStatus.ExitStatus())
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForExitCommand(cmd *exec.Cmd) (int, error) {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
// ignore errors
|
||||
cmd.Process.Signal(os.Kill) // #nosec G104
|
||||
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return exitError.ExitCode(), exitError
|
||||
}
|
||||
|
||||
return 2, err
|
||||
}
|
||||
|
||||
waitStatus, ok := cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return 2, fmt.Errorf("unexpected ProcessState type, expected syscall.WaitStatus, got %T", waitStatus)
|
||||
}
|
||||
return waitStatus.ExitStatus(), nil
|
||||
}
|
||||
|
||||
func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, expandSecrets bool, secretOverriding bool, token *models.TokenDetails) {
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var err error
|
||||
var lastSecretsFetch time.Time
|
||||
var lastUpdateEvent time.Time
|
||||
var watchMutex sync.Mutex
|
||||
var processMutex sync.Mutex
|
||||
var beingTerminated = false
|
||||
var currentETag string
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to fetch secrets")
|
||||
}
|
||||
|
||||
runCommandWithWatcher := func(environmentVariables models.InjectableEnvironmentResult) {
|
||||
currentETag = environmentVariables.ETag
|
||||
secretsFetchedAt := time.Now()
|
||||
if secretsFetchedAt.After(lastSecretsFetch) {
|
||||
lastSecretsFetch = secretsFetchedAt
|
||||
}
|
||||
|
||||
shouldRestartProcess := cmd != nil
|
||||
// terminate the old process before starting a new one
|
||||
if shouldRestartProcess {
|
||||
log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Environment changes detected. Reloading process..."))
|
||||
beingTerminated = true
|
||||
|
||||
log.Debug().Msgf(color.HiMagentaString("[HOT RELOAD] Sending SIGTERM to PID %d", cmd.Process.Pid))
|
||||
if e := cmd.Process.Signal(syscall.SIGTERM); e != nil {
|
||||
log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGTERM"))
|
||||
}
|
||||
// wait up to 10 sec for the process to exit
|
||||
for i := 0; i < 10; i++ {
|
||||
if !util.IsProcessRunning(cmd.Process) {
|
||||
// process has been killed so we break out
|
||||
break
|
||||
}
|
||||
if i == 5 {
|
||||
log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Still waiting for process exit status"))
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// SIGTERM may not work on Windows so we try SIGKILL
|
||||
if util.IsProcessRunning(cmd.Process) {
|
||||
log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Process still hasn't fully exited, attempting SIGKILL"))
|
||||
if e := cmd.Process.Kill(); e != nil {
|
||||
log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGKILL"))
|
||||
}
|
||||
}
|
||||
|
||||
cmd = nil
|
||||
} else {
|
||||
// If `cmd` is nil, we know this is the first time we are starting the process
|
||||
log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Watching for secret changes..."))
|
||||
}
|
||||
|
||||
processMutex.Lock()
|
||||
|
||||
if lastUpdateEvent.After(secretsFetchedAt) {
|
||||
processMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
beingTerminated = false
|
||||
watcherWaitGroup.Add(1)
|
||||
|
||||
// start the process
|
||||
log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environmentVariables.SecretsCount))
|
||||
|
||||
cmd, err = util.RunCommand(commandFlag, args, environmentVariables.Variables, false)
|
||||
if err != nil {
|
||||
defer watcherWaitGroup.Done()
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer processMutex.Unlock()
|
||||
defer watcherWaitGroup.Done()
|
||||
|
||||
exitCode, err := waitForExitCommand(cmd)
|
||||
|
||||
// ignore errors if we are being terminated
|
||||
if !beingTerminated {
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "exec") || strings.HasPrefix(err.Error(), "fork/exec") {
|
||||
log.Error().Err(err).Msg("Failed to execute command")
|
||||
}
|
||||
if err.Error() != ErrManualSignalInterrupt.Error() {
|
||||
log.Error().Err(err).Msg("Process exited with error")
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
recheckSecretsChannel := make(chan bool, 1)
|
||||
recheckSecretsChannel <- true
|
||||
|
||||
// a simple goroutine that triggers the recheckSecretsChan every watch interval (defaults to 10 seconds)
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Duration(watchModeInterval) * time.Second)
|
||||
recheckSecretsChannel <- true
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
<-recheckSecretsChannel
|
||||
watchMutex.Lock()
|
||||
|
||||
newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets")
|
||||
continue
|
||||
}
|
||||
|
||||
if newEnvironmentVariables.ETag != currentETag {
|
||||
runCommandWithWatcher(newEnvironmentVariables)
|
||||
} else {
|
||||
log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process")
|
||||
}
|
||||
|
||||
watchMutex.Unlock()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, shouldExpandSecrets bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) {
|
||||
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
request.InfisicalToken = token.Token
|
||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||
request.UniversalAuthAccessToken = token.Token
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir)
|
||||
|
||||
if err != nil {
|
||||
return models.InjectableEnvironmentResult{}, err
|
||||
}
|
||||
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
|
||||
} else {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
|
||||
}
|
||||
|
||||
if shouldExpandSecrets {
|
||||
|
||||
authParams := models.ExpandSecretsAuthentication{}
|
||||
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
authParams.InfisicalToken = token.Token
|
||||
} else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
|
||||
authParams.UniversalAuthAccessToken = token.Token
|
||||
}
|
||||
|
||||
secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
|
||||
}
|
||||
|
||||
secretsByKey := getSecretsByKeys(secrets)
|
||||
environmentVariables := make(map[string]string)
|
||||
|
||||
// add all existing environment vars
|
||||
for _, s := range os.Environ() {
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
key := kv[0]
|
||||
value := kv[1]
|
||||
environmentVariables[key] = value
|
||||
}
|
||||
|
||||
// check to see if there are any reserved key words in secrets to inject
|
||||
filterReservedEnvVars(secretsByKey)
|
||||
|
||||
// now add infisical secrets
|
||||
for k, v := range secretsByKey {
|
||||
environmentVariables[k] = v.Value
|
||||
}
|
||||
|
||||
env := make([]string, 0, len(environmentVariables))
|
||||
for key, value := range environmentVariables {
|
||||
env = append(env, key+"="+value)
|
||||
}
|
||||
|
||||
return models.InjectableEnvironmentResult{
|
||||
Variables: env,
|
||||
ETag: util.GenerateETagFromSecrets(secrets),
|
||||
SecretsCount: len(secretsByKey),
|
||||
}, nil
|
||||
}
|
||||
|
@@ -104,6 +104,12 @@ type GetAllSecretsParameters struct {
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
type InjectableEnvironmentResult struct {
|
||||
Variables []string
|
||||
ETag string
|
||||
SecretsCount int
|
||||
}
|
||||
|
||||
type GetAllFoldersParameters struct {
|
||||
WorkspaceId string
|
||||
Environment string
|
||||
|
92
cli/packages/util/exec.go
Normal file
92
cli/packages/util/exec.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func RunCommand(singleCommand string, args []string, env []string, waitForExit bool) (*exec.Cmd, error) {
|
||||
var c *exec.Cmd
|
||||
var err error
|
||||
|
||||
if singleCommand != "" {
|
||||
c, err = RunCommandFromString(singleCommand, env, waitForExit)
|
||||
} else {
|
||||
c, err = RunCommandFromArgs(args, env, waitForExit)
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
func IsProcessRunning(p *os.Process) bool {
|
||||
err := p.Signal(syscall.Signal(0))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// For "infisical run -- COMMAND"
|
||||
func RunCommandFromArgs(args []string, env []string, waitForExit bool) (*exec.Cmd, error) {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = env
|
||||
|
||||
err := execCommand(cmd, waitForExit)
|
||||
|
||||
return cmd, err
|
||||
}
|
||||
|
||||
func execCommand(cmd *exec.Cmd, waitForExit bool) error {
|
||||
sigChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChannel)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
sig := <-sigChannel
|
||||
_ = cmd.Process.Signal(sig) // process all sigs
|
||||
}
|
||||
}()
|
||||
|
||||
if !waitForExit {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
_ = cmd.Process.Signal(os.Kill)
|
||||
return fmt.Errorf("failed to wait for command termination: %v", err)
|
||||
}
|
||||
|
||||
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||
os.Exit(waitStatus.ExitStatus())
|
||||
return nil
|
||||
}
|
||||
|
||||
// For "infisical run --command=COMMAND"
|
||||
func RunCommandFromString(command string, env []string, waitForExit bool) (*exec.Cmd, error) {
|
||||
shell := [2]string{"sh", "-c"}
|
||||
if runtime.GOOS == "windows" {
|
||||
shell = [2]string{"cmd", "/C"}
|
||||
} else {
|
||||
currentShell := os.Getenv("SHELL")
|
||||
if currentShell != "" {
|
||||
shell[0] = currentShell
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(shell[0], shell[1], command) // #nosec G204 nosemgrep: semgrep_configs.prohibit-exec-command
|
||||
cmd.Env = env
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := execCommand(cmd, waitForExit)
|
||||
return cmd, err
|
||||
}
|
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
@@ -298,3 +299,16 @@ func GenerateRandomString(length int) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func GenerateETagFromSecrets(secrets []models.SingleEnvironmentVariable) string {
|
||||
sortedSecrets := SortSecretsByKeys(secrets)
|
||||
content := []byte{}
|
||||
|
||||
for _, secret := range sortedSecrets {
|
||||
content = append(content, []byte(secret.Key)...)
|
||||
content = append(content, []byte(secret.Value)...)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(content)
|
||||
return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:]))
|
||||
}
|
||||
|
@@ -47,20 +47,20 @@ $ infisical run -- npm run dev
|
||||
Used to fetch secrets via a [machine identity](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||
# Example
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages.
|
||||
```
|
||||
|
||||
<Info>
|
||||
Alternatively, you may use service tokens.
|
||||
|
||||
Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys).
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_TOKEN=<service-token>
|
||||
# Example
|
||||
export INFISICAL_TOKEN=<service-token>
|
||||
```
|
||||
|
||||
</Info>
|
||||
</Info>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="INFISICAL_DISABLE_UPDATE_CHECK">
|
||||
@@ -69,22 +69,30 @@ $ infisical run -- npm run dev
|
||||
To use, simply export this variable in the terminal before running this command.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
# Example
|
||||
export INFISICAL_DISABLE_UPDATE_CHECK=true
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--project-config-dir">
|
||||
<Accordion title="--watch">
|
||||
By passing the `watch` flag, you are telling the CLI to watch for changes that happen in your Infisical project.
|
||||
If secret changes happen, the command you provided will automatically be restarted with the new environment variables attached.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --watch -- printenv
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--project-config-dir">
|
||||
Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
infisical run --project-config-dir=/some-dir -- printenv
|
||||
# Example
|
||||
infisical run --project-config-dir=/some-dir -- printenv
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="--command">
|
||||
@@ -172,3 +180,19 @@ $ infisical run -- npm run dev
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
## Automatically reload command when secrets change
|
||||
|
||||
To automatically reload your command when secrets change, use the `--watch` flag.
|
||||
|
||||
```bash
|
||||
infisical run --watch -- npm run dev
|
||||
```
|
||||
|
||||
This will watch for changes in your secrets and automatically restart your command with the new secrets.
|
||||
When your command restarts, it will have the new environment variables injeceted into it.
|
||||
|
||||
<Note>
|
||||
Please note that this feature is intended for development purposes. It is not recommended to use this in production environments. Generally it's not recommended to automatically reload your application in production when remote changes are made.
|
||||
</Note>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user