Compare commits
91 Commits
infisical-
...
daniel/inv
Author | SHA1 | Date | |
---|---|---|---|
|
41c29d41e1 | ||
|
4de33190a9 | ||
|
7cfecb39e4 | ||
|
7524b83c29 | ||
|
7a41cdf51b | ||
|
17d99cb2cf | ||
|
bd0da0ff74 | ||
|
d2a54234f4 | ||
|
626262461a | ||
|
93ba29e57f | ||
|
1581aa088d | ||
|
ceab951bca | ||
|
2e3dcc50ae | ||
|
7b04c08fc7 | ||
|
70842b8e5e | ||
|
36e3e4c1b5 | ||
|
1384c8e855 | ||
|
f213c75ede | ||
|
6ade708e19 | ||
|
ce3af41ebc | ||
|
e442f10fa5 | ||
|
2e8ad18285 | ||
|
f03ca7f916 | ||
|
af1905a39e | ||
|
1321aa712f | ||
|
5ad00130ea | ||
|
ea5e8e29e6 | ||
|
e7f89bdfef | ||
|
d23a7e41f3 | ||
|
52a885716d | ||
|
3fc907f076 | ||
|
eaf10483c0 | ||
|
dcd0234fb5 | ||
|
4dda270e8e | ||
|
c1cb85b49f | ||
|
ed71e651f6 | ||
|
1a11dd954b | ||
|
5d3574d3f6 | ||
|
aa42aa05aa | ||
|
7a36badb23 | ||
|
9ce6fd3f8e | ||
|
a8fd83652d | ||
|
87a9a87dcd | ||
|
0b882ece8c | ||
|
e005e94165 | ||
|
0e07eaaa01 | ||
|
e10e313af3 | ||
|
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 | ||
|
27852607d1 | ||
|
956719f797 | ||
|
71b8c59050 | ||
|
15c5fe4095 | ||
|
5343c7af00 | ||
|
8c03c160a9 | ||
|
604b0467f9 | ||
|
a2b555dd81 | ||
|
9120367562 | ||
|
f509464947 | ||
|
07fd489982 | ||
|
f6d3831d6d | ||
|
d604ef2480 | ||
|
fe096772e0 |
@@ -72,3 +72,6 @@ PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
||||
WORKFLOW_SLACK_CLIENT_ID=
|
||||
WORKFLOW_SLACK_CLIENT_SECRET=
|
||||
|
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
@@ -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
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
@@ -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>;
|
||||
|
@@ -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
@@ -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
@@ -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
@@ -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>>;
|
@@ -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,
|
||||
|
@@ -103,6 +103,13 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
||||
|
||||
if (!email || !profile.firstName) {
|
||||
logger.info(
|
||||
{
|
||||
err: new Error("Invalid saml request. Missing email or first name"),
|
||||
profile
|
||||
},
|
||||
`email: ${email} firstName: ${profile.firstName as string}`
|
||||
);
|
||||
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -447,7 +447,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.",
|
||||
@@ -1037,14 +1039,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"
|
||||
|
@@ -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,
|
||||
|
@@ -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,6 +473,8 @@ export const registerRoutes = async (
|
||||
userAliasDAL,
|
||||
orgMembershipDAL,
|
||||
tokenService,
|
||||
permissionService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
projectMembershipDAL
|
||||
});
|
||||
@@ -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,7 +1,7 @@
|
||||
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";
|
||||
@@ -42,7 +42,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) => {
|
||||
@@ -148,7 +152,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({
|
||||
@@ -700,6 +708,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",
|
||||
|
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
@@ -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({
|
||||
|
@@ -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(
|
||||
|
@@ -41,6 +41,7 @@ import {
|
||||
TCreateCaDTO,
|
||||
TDeleteCaDTO,
|
||||
TGetCaCertDTO,
|
||||
TGetCaCertificateTemplatesDTO,
|
||||
TGetCaCertsDTO,
|
||||
TGetCaCsrDTO,
|
||||
TGetCaDTO,
|
||||
@@ -64,7 +65,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 +109,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notAfter,
|
||||
maxPathLength,
|
||||
keyAlgorithm,
|
||||
requireTemplateForIssuance,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
@@ -170,7 +172,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
serialNumber
|
||||
})
|
||||
}),
|
||||
requireTemplateForIssuance
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -302,7 +305,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 +330,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status });
|
||||
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status, requireTemplateForIssuance });
|
||||
|
||||
return updatedCa;
|
||||
};
|
||||
@@ -1077,6 +1088,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)) {
|
||||
@@ -1347,6 +1361,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);
|
||||
|
||||
@@ -1568,6 +1585,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,
|
||||
@@ -1580,6 +1631,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
signIntermediate,
|
||||
importCertToCa,
|
||||
issueCertFromCa,
|
||||
signCertFromCa
|
||||
signCertFromCa,
|
||||
getCaCertificateTemplates
|
||||
};
|
||||
};
|
||||
|
@@ -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 = {
|
||||
@@ -125,6 +127,10 @@ export type TSignCertFromCaDTO =
|
||||
notAfter?: string;
|
||||
} & 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()
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -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,17 +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);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
@@ -461,6 +468,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 +489,331 @@ 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) {
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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">;
|
@@ -1,111 +0,0 @@
|
||||
---
|
||||
title: "Certificate Templates"
|
||||
sidebarTitle: "Certificate Templates"
|
||||
description: "Learn how to use certificate templates to enforce policies."
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
In order to ensure your certificates follow certain policies, you can use certificate templates during the issuance and signing flows.
|
||||
|
||||
A certificate template is linked to a certificate authority. It contains custom policies for certificate fields, allowing you to define rules based on your security policies.
|
||||
|
||||
## Workflow
|
||||
|
||||
The typical workflow for using certificate templates consists of the following steps:
|
||||
|
||||
1. Creating a certificate template attached to an existing CA along with defining custom rules for certificate fields.
|
||||
2. Selecting the certificate template during the creation of new certificates.
|
||||
|
||||
<Note>
|
||||
Note that this workflow can be executed via the Infisical UI or manually such
|
||||
as via API.
|
||||
</Note>
|
||||
|
||||
## Guide to using Certificate Templates
|
||||
|
||||
In the following steps, we explore how to issue a X.509 certificate using a certificate template.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
|
||||
<Steps>
|
||||
<Step title="Creating the certificate template">
|
||||
To create a certificate template, head to your Project > Internal PKI > Certificate Templates and press **Create Certificate Template**.
|
||||
|
||||

|
||||
|
||||
Here, set the **Issuing CA** to the CA you want to issue certificates under when the certificate template is used.
|
||||
|
||||

|
||||
|
||||
Here's some guidance on each field:
|
||||
- Template Name: A descriptive name for the certificate template.
|
||||
- Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template.
|
||||
- Certificate Collection: The collection where certificates issued with this template will be added.
|
||||
- Common Name (CN): The regular expression used to validate the common name in certificate requests.
|
||||
- Alternative Names (SANs): The regular expression used to validate subject alternative names in certificate requests.
|
||||
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
|
||||
|
||||
</Step>
|
||||
<Step title="Using the certificate template">
|
||||
Once you have created the certificate template from step 1, you can select it when issuing certificates.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
<Steps>
|
||||
<Step title="Creating the certificate template">
|
||||
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/pki/certificate-templates \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"caId": "<string>",
|
||||
"pkiCollectionId": "<string>",
|
||||
"name": "<string>",
|
||||
"commonName": "<string>",
|
||||
"subjectAlternativeName": "<string>",
|
||||
"ttl": "<string>"
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"caId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "certificate-template-1",
|
||||
"commonName": "<string>",
|
||||
...
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Using the certificate template">
|
||||
To use the certificate template, attach the certificate template ID when invoking the API endpoint for [issuing](/api-reference/endpoints/certificates/issue-certificate) or [signing](/api-reference/endpoints/certificates/sign-certificate) new certificates.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/pki/certificates/issue-certificate \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"certificateTemplateId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"friendlyName": "my-new-certificate",
|
||||
"commonName": "CERT",
|
||||
...
|
||||
}'
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
@@ -25,7 +25,7 @@ graph TD
|
||||
|
||||
The typical workflow for managing certificates consists of the following steps:
|
||||
|
||||
1. Issuing a certificate under an intermediate CA with details like name and validity period.
|
||||
1. Issuing a certificate under an intermediate CA with details like name and validity period. As part of certificate issuance, you can either issue a certificate directly from a CA or do it via a certificate template.
|
||||
2. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow,
|
||||
you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed
|
||||
data structure issued by a CA containing a list of revoked certificates to check if a certificate has been revoked.
|
||||
@@ -43,28 +43,51 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
||||
<Tab title="Infisical UI">
|
||||
|
||||
<Steps>
|
||||
<Step title="Creating a certificate template">
|
||||
A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA and can also be bound to a certificate collection for alerting such that any certificate issued under the template is automatically added to the collection.
|
||||
|
||||
With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like `.*.acme.com` or perhaps that the max TTL cannot be more than 1 year.
|
||||
|
||||
Head to your Project > Certificate Authorities > Your Issuing CA and create a certificate template.
|
||||
|
||||

|
||||
|
||||
Here's some guidance on each field:
|
||||
|
||||
- Template Name: A name for the certificate template.
|
||||
- Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template.
|
||||
- Certificate Collection (Optional): The certificate collection that certificates should be added to when issued under the template.
|
||||
- Common Name (CN): A regular expression used to validate the common name in certificate requests.
|
||||
- Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests.
|
||||
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
|
||||
</Step>
|
||||
<Step title="Creating a certificate">
|
||||
To create a certificate, head to your Project > Internal PKI > Certificates and press **Create Certificate**.
|
||||
To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section.
|
||||
|
||||

|
||||

|
||||
|
||||
Here, set the **CA** to the CA you want to issue the certificate under and fill out details for the certificate.
|
||||
Here, set the **Certificate Template** to the template from step 1 and fill out the rest of the details for the certificate to be issued.
|
||||
|
||||

|
||||

|
||||
|
||||
Here's some guidance on each field:
|
||||
|
||||
- Issuing CA: The CA under which to issue the certificate.
|
||||
- Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty.
|
||||
- Common Name (CN): The (common) name for the certificate like `service.acme.com`.
|
||||
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
|
||||
- TTL: The lifetime of the certificate in seconds.
|
||||
|
||||
|
||||
<Note>
|
||||
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
||||
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
||||
|
||||
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Copying the certificate details">
|
||||
Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**.
|
||||
|
||||

|
||||

|
||||
|
||||
<Note>
|
||||
Make sure to download and store the **Private Key** in a secure location as it will only be displayed once at the time of certificate issuance.
|
||||
@@ -74,16 +97,54 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
||||
|
||||
<Steps>
|
||||
<Step title="Creating a certificate template">
|
||||
A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA and can also be bound to a certificate collection for alerting such that any certificate issued under the template is automatically added to the collection.
|
||||
|
||||
With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year.
|
||||
|
||||
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificate-templates' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"caId": "<ca-id>",
|
||||
"name": "My Certificate Template",
|
||||
"commonName": ".*.acme.com",
|
||||
"subjectAlternativeName": ".*.acme.com",
|
||||
"ttl": "1y",
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
id: "...",
|
||||
caId: "...",
|
||||
name: "...",
|
||||
commonName: "...",
|
||||
subjectAlternativeName: "...",
|
||||
ttl: "...",
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Creating a certificate">
|
||||
To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
||||
specifying the issuing CA.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/issue-certificate' \
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/issue-certificate' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"commonName": "My Certificate",
|
||||
"certificateTemplateId": "<certificate-template-id>",
|
||||
"commonName": "service.acme.com",
|
||||
"ttl": "1y",
|
||||
}'
|
||||
```
|
||||
@@ -100,18 +161,26 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
||||
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
||||
|
||||
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
|
||||
</Note>
|
||||
|
||||
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA.
|
||||
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-certificate) API endpoint, specifying the issuing CA.
|
||||
|
||||
### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/sign-certificate' \
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/sign-certificate' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"certificateTemplateId": "<certificate-template-id>",
|
||||
"csr": "...",
|
||||
"ttl": "1y",
|
||||
}'
|
||||
@@ -128,7 +197,8 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
||||
serialNumber: "..."
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
@@ -26,7 +26,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g.
|
||||
|
||||
## Guide to configuring EST
|
||||
|
||||
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates).
|
||||
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificates#guide-to-issuing-certificates).
|
||||
|
||||
2. Proceed to the certificate template's enrollment settings
|
||||

|
||||
|
@@ -214,10 +214,13 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
|
||||
|
||||
Data
|
||||
====
|
||||
ca.crt: 1306 bytes
|
||||
tls.crt: 2380 bytes
|
||||
tls.key: 227 bytes
|
||||
tls.crt: 912 bytes
|
||||
```
|
||||
|
||||
Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate.
|
||||
|
||||
We can decode the certificate and print it out using `openssl`:
|
||||
|
||||
```bash
|
||||
|
@@ -66,6 +66,7 @@ consisting of an (optional) root CA and an intermediate CA.
|
||||
- State or Province Name: The state or province.
|
||||
- Locality Name: The city or locality.
|
||||
- Common Name: The name of the CA.
|
||||
- Require Template for Certificate Issuance: Whether or not certificates for this CA can only be issued through certificate templates (recommended).
|
||||
|
||||
<Note>
|
||||
The Organization, Country, State or Province Name, Locality Name, and Common Name make up the **Distinguished Name (DN)** or **subject** of the CA.
|
||||
|
@@ -49,8 +49,8 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
|
||||
Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **Attributes & Claims** section and configure the following map:
|
||||
|
||||
- `email -> user.userprinciplename`
|
||||
- `firstName -> user.firstName`
|
||||
- `lastName -> user.lastName`
|
||||
- `firstName -> user.givenname`
|
||||
- `lastName -> user.surname`
|
||||
|
||||

|
||||
|
||||
|
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: "Slack integration"
|
||||
description: "Learn how to setup Slack integration"
|
||||
---
|
||||
|
||||
This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical Cloud">
|
||||
## Create Slack workflow integration
|
||||
<Steps>
|
||||
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
|
||||
In order to use Slack integration in your projects, you will first have to
|
||||
configure a Slack workflow integration in your organization.
|
||||

|
||||
</Step>
|
||||
<Step title="Install Slack app to workspace">
|
||||
Press "Add" and select "Slack" as the platform.
|
||||

|
||||
|
||||
Give your Slack integration a descriptive alias. You will use this to select the Slack integration for your project.
|
||||

|
||||
|
||||
Press **Connect Slack**. This opens up the Slack app installation flow. Select the Slack workspace you want to install the custom Slack app to and press **Allow**.
|
||||

|
||||
|
||||
This completes the workflow integration creation flow. The projects in your organization can now use this Slack integration to send real-time updates to your Slack workspace.
|
||||

|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Configure project to use Slack workflow integration
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||

|
||||
</Step>
|
||||
<Step title="Select the Slack integration to use for the project">
|
||||
Your project will send notifications to the connected Slack workspace of the
|
||||
selected Slack integration when the configured events are triggered.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure the Slack notification settings for the project and click Save.">
|
||||

|
||||
<Info>
|
||||
To enable notifications in private Slack channels, you need to invite the Infisical Slack bot to join those channels.
|
||||
</Info>
|
||||
You now have a working native integration with Slack!
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Self-hosted setup">
|
||||
## Configure admin settings
|
||||
Note that this step only has to be done once for the entire instance.
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the Integrations tab in the Admin settings">
|
||||
Before anything else, you need to setup the Slack app to be used by
|
||||
your Infisical instance. Because you're self-hosting, you will need to
|
||||
create this Slack application as demonstrated in the preceding step.
|
||||

|
||||
</Step>
|
||||
<Step title="Create Slack app">
|
||||
Click the "Create Slack app" button. This will open up a new window with the
|
||||
custom app creation flow on Slack.
|
||||

|
||||
|
||||
Select the Slack workspace you want to integrate with Infisical.
|
||||
|
||||

|
||||
|
||||
The configuration values of your custom Slack app will be pre-filled for you. You can view or edit the app manifest by clicking **Edit Configurations**.
|
||||

|
||||
|
||||
Once everything's confirmed, press Create.
|
||||
|
||||
</Step>
|
||||
<Step title="Input app credentials from Slack">
|
||||
Copy the Client ID and Client Secret values from your newly created custom Slack app and add them to Infisical.
|
||||

|
||||

|
||||
Complete the admin setup by pressing Save.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Create Slack workflow integration
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
|
||||
In order to use Slack integration in your projects, you will first have to
|
||||
configure a Slack workflow integration in your organization.
|
||||

|
||||
</Step>
|
||||
<Step title="Install Slack app to workspace">
|
||||
Press "Add" and select "Slack" as the platform.
|
||||

|
||||
|
||||
Give your Slack integration a descriptive alias. You will use this to select the Slack integration for your project.
|
||||

|
||||
|
||||
Press **Connect Slack**. This opens up the Slack app installation flow. Select the Slack workspace you want to install the custom Slack app to and press **Allow**.
|
||||

|
||||
|
||||
Your Slack bot will then be added to your selected Slack workspace. This completes the workflow integration creation flow. Your projects in the organization can now use this Slack integration to send real-time updates to your Slack workspace.
|
||||

|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Configure project to use Slack workflow integration
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||

|
||||
</Step>
|
||||
<Step title="Select the Slack integration to use for the project">
|
||||
Your project will send notifications to the connected Slack workspace of the
|
||||
selected Slack integration when the configured events are triggered.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure the Slack notification settings for the project and click Save.">
|
||||

|
||||
<Info>
|
||||
To enable notifications in private Slack channels, you need to invite your Slack bot to join those channels.
|
||||
</Info>
|
||||
You now have a working native integration with Slack!
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
Before Width: | Height: | Size: 721 KiB |
Before Width: | Height: | Size: 372 KiB |
Before Width: | Height: | Size: 579 KiB |
Before Width: | Height: | Size: 715 KiB |
Before Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 97 KiB |
BIN
docs/images/platform/pki/certificate/cert-body.png
Normal file
After Width: | Height: | Size: 779 KiB |
BIN
docs/images/platform/pki/certificate/cert-issue-modal.png
Normal file
After Width: | Height: | Size: 379 KiB |
BIN
docs/images/platform/pki/certificate/cert-issue.png
Normal file
After Width: | Height: | Size: 700 KiB |
BIN
docs/images/platform/pki/certificate/cert-template-modal.png
Normal file
After Width: | Height: | Size: 517 KiB |
Before Width: | Height: | Size: 462 KiB |
Before Width: | Height: | Size: 723 KiB After Width: | Height: | Size: 865 KiB |
After Width: | Height: | Size: 688 KiB |
After Width: | Height: | Size: 516 KiB |
After Width: | Height: | Size: 439 KiB |
After Width: | Height: | Size: 341 KiB |
After Width: | Height: | Size: 687 KiB |
After Width: | Height: | Size: 531 KiB |
After Width: | Height: | Size: 247 KiB |
After Width: | Height: | Size: 895 KiB |
After Width: | Height: | Size: 377 KiB |
After Width: | Height: | Size: 209 KiB |
After Width: | Height: | Size: 520 KiB |
After Width: | Height: | Size: 348 KiB |