mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
266 Commits
update-sel
...
mongo-to-p
Author | SHA1 | Date | |
---|---|---|---|
646ceba089 | |||
9d202e8501 | |||
1f9f15136e | |||
4bf7e8bbd1 | |||
6891d309da | |||
1cccbca0c5 | |||
2c2e1f5d2e | |||
6946f3901c | |||
82a7010e29 | |||
a1e763fa28 | |||
0992117173 | |||
9419884a26 | |||
850f3a347c | |||
4c9101d18d | |||
06e8e90ad5 | |||
fdd79c0568 | |||
4ef8abdb00 | |||
46f0fb7a41 | |||
2142f5736c | |||
ce764d70ad | |||
c2d0ddb2fc | |||
7ba9588509 | |||
cddb09e031 | |||
046dc83638 | |||
320074ef6c | |||
e780ee6573 | |||
a5a881c382 | |||
200d4a5af6 | |||
07318ec54b | |||
92d237a714 | |||
6ef988fa86 | |||
70822d0d98 | |||
e91499b301 | |||
92acb4d943 | |||
76daa20d69 | |||
a231813f01 | |||
3eb2bdb191 | |||
cadf6e1157 | |||
ceb7fafc06 | |||
3063bb9982 | |||
3d82a43615 | |||
028541a18a | |||
66a631ff46 | |||
28adb8f0ac | |||
5c988c2cd5 | |||
acf8a54abb | |||
387094aa27 | |||
4251e95c15 | |||
f4386c2d93 | |||
ff4b943854 | |||
879a5ecfac | |||
a831a7d848 | |||
3138784d1a | |||
0b258e3918 | |||
d0545a01b9 | |||
d71398344f | |||
25e3cc047b | |||
17149de567 | |||
cca2fb7ff5 | |||
f1f2d62993 | |||
be49de5f34 | |||
acfa89ba8b | |||
389ec85554 | |||
2a6b0efe22 | |||
9c67d43ebe | |||
d8f3531b50 | |||
57be73c17e | |||
a10129e750 | |||
adc10cf675 | |||
49f7780e52 | |||
26482c6b0a | |||
1cf9aaeb1b | |||
fed022ed09 | |||
64fbe4161c | |||
bbe769a961 | |||
31cc3ece0c | |||
52cfa1ba39 | |||
4553c6bb37 | |||
554f0cfd00 | |||
0a5112d302 | |||
bdb0ed3e5e | |||
7816d8593e | |||
816c793ae3 | |||
9f0d09f8ed | |||
2cbd2ee75f | |||
368974cf01 | |||
8be976a694 | |||
cab47d0b98 | |||
aa81711824 | |||
10fbb99a15 | |||
4657985468 | |||
68ac1d285a | |||
fe7524fca1 | |||
bf9b47ad66 | |||
8e49825e16 | |||
27b4749205 | |||
5b1f07a661 | |||
50128bbac6 | |||
debf80cfdc | |||
4ab47ca175 | |||
021413fbd9 | |||
8a39276e04 | |||
b5e64bc8b8 | |||
faa842c3d2 | |||
28b24115b7 | |||
198dc05753 | |||
178492e9bd | |||
fb9cdb591c | |||
4c5100de6b | |||
b587e6a4a1 | |||
773756d731 | |||
9efece1f01 | |||
bb6e8b1a51 | |||
0f98fc94f0 | |||
7f1963f1ac | |||
6064c393c6 | |||
0cecf05a5b | |||
dc6497f9eb | |||
e445970f36 | |||
c33741d588 | |||
5dfc84190d | |||
a1d11c0fcd | |||
863bbd420c | |||
4b37b2afba | |||
a366dbb16d | |||
423ad49490 | |||
2a4bda481d | |||
5b550a97a1 | |||
0fa0e4eb0f | |||
65e3f0ec95 | |||
c20f6e51ae | |||
cee8ead78a | |||
82fe0bb5c4 | |||
0b7efa57be | |||
9c11226b71 | |||
ae3606c9fb | |||
a0e25b8ea2 | |||
0931a17af5 | |||
c16bf2afdb | |||
04b4e80dd1 | |||
f178220c5a | |||
ed353d3263 | |||
ec6ec8813e | |||
3ea529d525 | |||
f35f10558b | |||
28287b8ed4 | |||
0f3ec51d14 | |||
75813deb81 | |||
66e57d5d11 | |||
fb2a213214 | |||
c0b11b8350 | |||
bea24d9654 | |||
a7bc62f8e4 | |||
2ef7e8f58e | |||
41d3b9314e | |||
1e9d49008b | |||
49d07a6762 | |||
9ce71371a9 | |||
c1c66da92b | |||
4121c1d573 | |||
108f3cf117 | |||
a6e263eded | |||
419916ee0c | |||
f7e6a96a02 | |||
b0356ba941 | |||
7ea5323a37 | |||
23e198d891 | |||
9f9849ccfd | |||
0c53eb8e22 | |||
9b62937db2 | |||
ebb8d632c4 | |||
43aae87fb0 | |||
3415514fde | |||
c0e0ddde76 | |||
39ae66a84f | |||
e8ec5b8b49 | |||
592271de3b | |||
5680b984cf | |||
f378d6cc2b | |||
04c12d9a75 | |||
31b5f779fb | |||
bb92cef764 | |||
6090f86b74 | |||
8c3569a047 | |||
6fa11fe637 | |||
9287eb7031 | |||
e54b261c0f | |||
60747b10b6 | |||
bf278355c4 | |||
d3d429db37 | |||
f2dcc83a56 | |||
26576b6bcd | |||
4cca82c3c8 | |||
1b82a157cc | |||
5409cffe33 | |||
45327f10b1 | |||
37645ba126 | |||
858b49d766 | |||
a3a1a0007d | |||
075f457bd1 | |||
5156971d75 | |||
8f3de3cc90 | |||
69cba4e6c7 | |||
6dcab6646c | |||
8e13eb6077 | |||
819a9b8d27 | |||
ec3cf0208c | |||
4aa5822ae2 | |||
5364480ca2 | |||
4802a36473 | |||
8333250b0b | |||
0cfab8ab6b | |||
8fd99855bd | |||
f2c36c58f9 | |||
f47fdfe386 | |||
8a11eebab8 | |||
3b1fc4b156 | |||
84cab17f5c | |||
db773864d5 | |||
b9840ceba9 | |||
729ec7866a | |||
a7140941ee | |||
34d1bbc2ed | |||
3ad0382cb0 | |||
ccc409e9cd | |||
fe21ba0e54 | |||
80a802386c | |||
aec0e86182 | |||
8e3cddc1ea | |||
3612e5834c | |||
031a2416a9 | |||
2eb9592b1a | |||
bbd9fa4a56 | |||
318ad25c11 | |||
c372eb7d20 | |||
68a99a0b32 | |||
7512231e20 | |||
f0e580d68b | |||
116015d3cf | |||
308ff50197 | |||
9df5cbbe85 | |||
a714a64bc2 | |||
ea18d99793 | |||
7c098529f7 | |||
e20c623e91 | |||
72d4490ee7 | |||
2336a7265b | |||
6f9b30b46e | |||
7a65f8c837 | |||
bccbedfc31 | |||
0ab811194d | |||
7b54109168 | |||
97d2a15d3e | |||
e15ed4cc58 | |||
a1cc118514 | |||
c73ee49425 | |||
ee69bccb6e | |||
0ff3ddb0c8 | |||
3a7b697549 | |||
0fb87ab05f | |||
2ef8781378 | |||
3f96f0a8fb | |||
da377f6fda | |||
5cf1ec2400 | |||
6c1489a87b | |||
d5064fe75a |
@ -19,10 +19,6 @@ POSTGRES_DB=infisical
|
|||||||
# Redis
|
# Redis
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
# Optional credentials for MongoDB container instance and Mongo-Express
|
|
||||||
MONGO_USERNAME=root
|
|
||||||
MONGO_PASSWORD=example
|
|
||||||
|
|
||||||
# Website URL
|
# Website URL
|
||||||
# Required
|
# Required
|
||||||
SITE_URL=http://localhost:8080
|
SITE_URL=http://localhost:8080
|
||||||
|
@ -72,4 +72,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker-compose -f "docker-compose.dev.yml" down
|
docker-compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api
|
||||||
docker remove infisical-api
|
docker remove infisical-api
|
@ -1,4 +1,4 @@
|
|||||||
name: Check Frontend Pull Request
|
name: Check Frontend Type and Lint check
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -10,8 +10,8 @@ on:
|
|||||||
- "frontend/.eslintrc.js"
|
- "frontend/.eslintrc.js"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-fe-pr:
|
check-fe-ts-lint:
|
||||||
name: Check
|
name: Check Frontend Type and Lint check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
@ -25,12 +25,11 @@ jobs:
|
|||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
run: npm ci --only-production --ignore-scripts
|
run: npm install
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
# -
|
- name: 🏗️ Run Type check
|
||||||
# name: 🧪 Run tests
|
run: npm run type:check
|
||||||
# run: npm run test:ci
|
working-directory: frontend
|
||||||
# working-directory: frontend
|
- name: 🏗️ Run Link check
|
||||||
- name: 🏗️ Run build
|
run: npm run lint:fix
|
||||||
run: npm run build
|
|
||||||
working-directory: frontend
|
working-directory: frontend
|
2
.github/workflows/run-backend-tests.yml
vendored
2
.github/workflows/run-backend-tests.yml
vendored
@ -44,4 +44,4 @@ jobs:
|
|||||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
run: |
|
run: |
|
||||||
docker-compose -f "docker-compose.dev.yml" down
|
docker-compose -f "docker-compose.dev.yml" down
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -63,3 +63,5 @@ yarn-error.log*
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
||||||
frontend-build
|
frontend-build
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
Thanks for taking the time to contribute! 😃 🚀
|
Thanks for taking the time to contribute! 😃 🚀
|
||||||
|
|
||||||
Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/overview) for instructions on how to contribute.
|
Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/getting-started/overview) for instructions on how to contribute.
|
||||||
|
|
||||||
We also have some 🔥amazing🔥 merch for our contributors. Please reach out to tony@infisical.com for more info 👀
|
We also have some 🔥amazing🔥 merch for our contributors. Please reach out to tony@infisical.com for more info 👀
|
||||||
|
@ -56,6 +56,7 @@ export default {
|
|||||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("[TEST] Error setting up environment", error);
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
728
backend/package-lock.json
generated
728
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -70,17 +70,17 @@
|
|||||||
"vitest": "^1.2.2"
|
"vitest": "^1.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-secrets-manager": "^3.485.0",
|
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@fastify/cookie": "^9.2.0",
|
"@fastify/cookie": "^9.2.0",
|
||||||
"@fastify/cors": "^8.4.1",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/etag": "^5.1.0",
|
"@fastify/etag": "^5.1.0",
|
||||||
"@fastify/formbody": "^7.4.0",
|
"@fastify/formbody": "^7.4.0",
|
||||||
"@fastify/helmet": "^11.1.1",
|
"@fastify/helmet": "^11.1.1",
|
||||||
"@fastify/passport": "^2.4.0",
|
"@fastify/passport": "^2.4.0",
|
||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
@ -90,13 +90,13 @@
|
|||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"argon2": "^0.31.2",
|
"argon2": "^0.31.2",
|
||||||
"aws-sdk": "^2.1545.0",
|
"aws-sdk": "^2.1549.0",
|
||||||
"axios": "^1.6.4",
|
"axios": "^1.6.7",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.1.1",
|
"bullmq": "^5.1.6",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.24.3",
|
"fastify": "^4.26.0",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
@ -106,7 +106,7 @@
|
|||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"mysql2": "^3.6.5",
|
"mysql2": "^3.9.1",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
@ -124,6 +124,6 @@
|
|||||||
"tweetnacl-util": "^0.15.1",
|
"tweetnacl-util": "^0.15.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zod-to-json-schema": "^3.22.0"
|
"zod-to-json-schema": "^3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -6,6 +6,7 @@ import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
|
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
@ -105,6 +106,7 @@ declare module "fastify" {
|
|||||||
secretRotation: TSecretRotationServiceFactory;
|
secretRotation: TSecretRotationServiceFactory;
|
||||||
snapshot: TSecretSnapshotServiceFactory;
|
snapshot: TSecretSnapshotServiceFactory;
|
||||||
saml: TSamlConfigServiceFactory;
|
saml: TSamlConfigServiceFactory;
|
||||||
|
scim: TScimServiceFactory;
|
||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
|
4
backend/src/@types/knex.d.ts
vendored
4
backend/src/@types/knex.d.ts
vendored
@ -83,6 +83,9 @@ import {
|
|||||||
TSamlConfigs,
|
TSamlConfigs,
|
||||||
TSamlConfigsInsert,
|
TSamlConfigsInsert,
|
||||||
TSamlConfigsUpdate,
|
TSamlConfigsUpdate,
|
||||||
|
TScimTokens,
|
||||||
|
TScimTokensInsert,
|
||||||
|
TScimTokensUpdate,
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
TSecretApprovalPoliciesApprovers,
|
TSecretApprovalPoliciesApprovers,
|
||||||
TSecretApprovalPoliciesApproversInsert,
|
TSecretApprovalPoliciesApproversInsert,
|
||||||
@ -262,6 +265,7 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityProjectMembershipsInsert,
|
TIdentityProjectMembershipsInsert,
|
||||||
TIdentityProjectMembershipsUpdate
|
TIdentityProjectMembershipsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
TSecretApprovalPoliciesInsert,
|
TSecretApprovalPoliciesInsert,
|
||||||
|
31
backend/src/db/migrations/20240208234120_scim-token.ts
Normal file
31
backend/src/db/migrations/20240208234120_scim-token.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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.ScimToken))) {
|
||||||
|
await knex.schema.createTable(TableName.ScimToken, (t) => {
|
||||||
|
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.bigInteger("ttlDays").defaultTo(365).notNullable();
|
||||||
|
t.string("description").notNullable();
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.boolean("scimEnabled").defaultTo(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ScimToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ScimToken);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ScimToken);
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("scimEnabled");
|
||||||
|
});
|
||||||
|
}
|
39
backend/src/db/migrations/20240216154123_ghost_users.ts
Normal file
39
backend/src/db/migrations/20240216154123_ghost_users.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ProjectVersion, TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost");
|
||||||
|
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
|
||||||
|
|
||||||
|
if (!hasGhostUserColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
t.boolean("isGhost").defaultTo(false).notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasProjectVersionColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.integer("version").defaultTo(ProjectVersion.V1).notNullable();
|
||||||
|
t.string("upgradeStatus").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost");
|
||||||
|
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
|
||||||
|
|
||||||
|
if (hasGhostUserColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
t.dropColumn("isGhost");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasProjectVersionColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.dropColumn("version");
|
||||||
|
t.dropColumn("upgradeStatus");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const isTablePresent = await knex.schema.hasTable(TableName.SuperAdmin);
|
||||||
|
if (isTablePresent) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
t.string("allowedSignUpDomain");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SuperAdmin, "allowedSignUpDomain")) {
|
||||||
|
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||||
|
t.dropColumn("allowedSignUpDomain");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ export * from "./project-memberships";
|
|||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
|
export * from "./scim-tokens";
|
||||||
export * from "./secret-approval-policies";
|
export * from "./secret-approval-policies";
|
||||||
export * from "./secret-approval-policies-approvers";
|
export * from "./secret-approval-policies-approvers";
|
||||||
export * from "./secret-approval-request-secret-tags";
|
export * from "./secret-approval-request-secret-tags";
|
||||||
|
@ -40,6 +40,7 @@ export enum TableName {
|
|||||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||||
IdentityOrgMembership = "identity_org_memberships",
|
IdentityOrgMembership = "identity_org_memberships",
|
||||||
IdentityProjectMembership = "identity_project_memberships",
|
IdentityProjectMembership = "identity_project_memberships",
|
||||||
|
ScimToken = "scim_tokens",
|
||||||
SecretApprovalPolicy = "secret_approval_policies",
|
SecretApprovalPolicy = "secret_approval_policies",
|
||||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||||
SecretApprovalRequest = "secret_approval_requests",
|
SecretApprovalRequest = "secret_approval_requests",
|
||||||
@ -111,6 +112,17 @@ export enum SecretType {
|
|||||||
Personal = "personal"
|
Personal = "personal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectVersion {
|
||||||
|
V1 = 1,
|
||||||
|
V2 = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProjectUpgradeStatus {
|
||||||
|
InProgress = "IN_PROGRESS",
|
||||||
|
// Completed -> Will be null if completed. So a completed status is not needed
|
||||||
|
Failed = "FAILED"
|
||||||
|
}
|
||||||
|
|
||||||
export enum IdentityAuthMethod {
|
export enum IdentityAuthMethod {
|
||||||
Univeral = "universal-auth"
|
Univeral = "universal-auth"
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
authEnforced: z.boolean().default(false).nullable().optional()
|
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||||
|
scimEnabled: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
@ -14,7 +14,9 @@ export const ProjectsSchema = z.object({
|
|||||||
autoCapitalization: z.boolean().default(true).nullable().optional(),
|
autoCapitalization: z.boolean().default(true).nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
version: z.number().default(1),
|
||||||
|
upgradeStatus: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
21
backend/src/db/schemas/scim-tokens.ts
Normal file
21
backend/src/db/schemas/scim-tokens.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 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 ScimTokensSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
ttlDays: z.coerce.number().default(365),
|
||||||
|
description: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TScimTokens = z.infer<typeof ScimTokensSchema>;
|
||||||
|
export type TScimTokensInsert = Omit<TScimTokens, TImmutableDBKeys>;
|
||||||
|
export type TScimTokensUpdate = Partial<Omit<TScimTokens, TImmutableDBKeys>>;
|
@ -12,7 +12,8 @@ export const SuperAdminSchema = z.object({
|
|||||||
initialized: z.boolean().default(false).nullable().optional(),
|
initialized: z.boolean().default(false).nullable().optional(),
|
||||||
allowSignUp: z.boolean().default(true).nullable().optional(),
|
allowSignUp: z.boolean().default(true).nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
allowedSignUpDomain: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||||
|
@ -19,7 +19,8 @@ export const UsersSchema = z.object({
|
|||||||
mfaMethods: z.string().array().nullable().optional(),
|
mfaMethods: z.string().array().nullable().optional(),
|
||||||
devices: z.unknown().nullable().optional(),
|
devices: z.unknown().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
isGhost: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable import/no-mutable-exports */
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import argon2, { argon2id } from "argon2";
|
import argon2, { argon2id } from "argon2";
|
||||||
@ -15,9 +16,12 @@ import {
|
|||||||
|
|
||||||
import { TSecrets, TUserEncryptionKeys } from "./schemas";
|
import { TSecrets, TUserEncryptionKeys } from "./schemas";
|
||||||
|
|
||||||
|
export let userPrivateKey: string | undefined;
|
||||||
|
export let userPublicKey: string | undefined;
|
||||||
|
|
||||||
export const seedData1 = {
|
export const seedData1 = {
|
||||||
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
||||||
email: "test@localhost.local",
|
email: process.env.TEST_USER_EMAIL || "test@localhost.local",
|
||||||
password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
|
password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
|
||||||
organization: {
|
organization: {
|
||||||
id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
|
id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
|
||||||
@ -42,6 +46,12 @@ export const seedData1 = {
|
|||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
||||||
|
},
|
||||||
|
|
||||||
|
// We set these values during user creation, and later re-use them during project seeding.
|
||||||
|
encryptionKeys: {
|
||||||
|
publicKey: "",
|
||||||
|
privateKey: ""
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { registerOrgRoleRouter } from "./org-role-router";
|
|||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerSamlRouter } from "./saml-router";
|
import { registerSamlRouter } from "./saml-router";
|
||||||
|
import { registerScimRouter } from "./scim-router";
|
||||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||||
@ -33,6 +34,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
prefix: "/secret-rotation-providers"
|
prefix: "/secret-rotation-providers"
|
||||||
});
|
});
|
||||||
await server.register(registerSamlRouter, { prefix: "/sso" });
|
await server.register(registerSamlRouter, { prefix: "/sso" });
|
||||||
|
await server.register(registerScimRouter, { prefix: "/scim" });
|
||||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
|
331
backend/src/ee/routes/v1/scim-router.ts
Normal file
331
backend/src/ee/routes/v1/scim-router.ts
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ScimTokensSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
|
||||||
|
try {
|
||||||
|
const strBody = body instanceof Buffer ? body.toString() : body;
|
||||||
|
|
||||||
|
const json: unknown = JSON.parse(strBody);
|
||||||
|
done(null, json);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
done(error, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/scim-tokens",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
description: z.string().trim().default(""),
|
||||||
|
ttlDays: z.number().min(0).default(0)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
scimToken: z.string().trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { scimToken } = await server.services.scim.createScimToken({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
description: req.body.description,
|
||||||
|
ttlDays: req.body.ttlDays
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scimToken };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/scim-tokens",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
scimTokens: z.array(ScimTokensSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const scimTokens = await server.services.scim.listScimTokens({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
orgId: req.query.organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scimTokens };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/scim-tokens/:scimTokenId",
|
||||||
|
method: "DELETE",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
scimTokenId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
scimToken: ScimTokensSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const scimToken = await server.services.scim.deleteScimToken({
|
||||||
|
scimTokenId: req.params.scimTokenId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scimToken };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// SCIM server endpoints
|
||||||
|
server.route({
|
||||||
|
url: "/Users",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
startIndex: z.coerce.number().default(1),
|
||||||
|
count: z.coerce.number().default(20),
|
||||||
|
filter: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
Resources: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
itemsPerPage: z.number(),
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
startIndex: z.number(),
|
||||||
|
totalResults: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const users = await req.server.services.scim.listScimUsers({
|
||||||
|
offset: req.query.startIndex,
|
||||||
|
limit: req.query.count,
|
||||||
|
filter: req.query.filter,
|
||||||
|
orgId: req.permission.orgId as string
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users/:userId",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
201: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.getScimUser({
|
||||||
|
userId: req.params.userId,
|
||||||
|
orgId: req.permission.orgId as string
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
userName: z.string().trim().email(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
// emails: z.array( // optional?
|
||||||
|
// z.object({
|
||||||
|
// primary: z.boolean(),
|
||||||
|
// value: z.string().email(),
|
||||||
|
// type: z.string().trim()
|
||||||
|
// })
|
||||||
|
// ),
|
||||||
|
// displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim().email(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.createScimUser({
|
||||||
|
email: req.body.userName,
|
||||||
|
firstName: req.body.name.givenName,
|
||||||
|
lastName: req.body.name.familyName,
|
||||||
|
orgId: req.permission.orgId as string
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users/:userId",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
Operations: z.array(
|
||||||
|
z.object({
|
||||||
|
op: z.string().trim(),
|
||||||
|
path: z.string().trim().optional(),
|
||||||
|
value: z.union([
|
||||||
|
z.object({
|
||||||
|
active: z.boolean()
|
||||||
|
}),
|
||||||
|
z.string().trim()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.updateScimUser({
|
||||||
|
userId: req.params.userId,
|
||||||
|
orgId: req.permission.orgId as string,
|
||||||
|
operations: req.body.Operations
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users/:userId",
|
||||||
|
method: "PUT",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.replaceScimUser({
|
||||||
|
userId: req.params.userId,
|
||||||
|
orgId: req.permission.orgId as string,
|
||||||
|
active: req.body.active
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -58,6 +58,7 @@ export const auditLogServiceFactory = ({
|
|||||||
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
||||||
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return auditLogQueue.pushToLog(data);
|
return auditLogQueue.pushToLog(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export type TListProjectAuditLogDTO = {
|
|||||||
|
|
||||||
export type TCreateAuditLogDTO = {
|
export type TCreateAuditLogDTO = {
|
||||||
event: Event;
|
event: Event;
|
||||||
actor: UserActor | IdentityActor | ServiceActor;
|
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
} & BaseAuthData;
|
} & BaseAuthData;
|
||||||
@ -105,6 +105,8 @@ interface IdentityActorMetadata {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScimClientActorMetadata {}
|
||||||
|
|
||||||
export interface UserActor {
|
export interface UserActor {
|
||||||
type: ActorType.USER;
|
type: ActorType.USER;
|
||||||
metadata: UserActorMetadata;
|
metadata: UserActorMetadata;
|
||||||
@ -120,7 +122,12 @@ export interface IdentityActor {
|
|||||||
metadata: IdentityActorMetadata;
|
metadata: IdentityActorMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Actor = UserActor | ServiceActor | IdentityActor;
|
export interface ScimClientActor {
|
||||||
|
type: ActorType.SCIM_CLIENT;
|
||||||
|
metadata: ScimClientActorMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
|
||||||
|
|
||||||
interface GetSecretsEvent {
|
interface GetSecretsEvent {
|
||||||
type: EventType.GET_SECRETS;
|
type: EventType.GET_SECRETS;
|
||||||
|
@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
|
scim: false,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
|
@ -25,6 +25,7 @@ export type TFeatureSet = {
|
|||||||
auditLogs: false;
|
auditLogs: false;
|
||||||
auditLogsRetentionDays: 0;
|
auditLogsRetentionDays: 0;
|
||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
|
scim: false;
|
||||||
status: null;
|
status: null;
|
||||||
trial_end: null;
|
trial_end: null;
|
||||||
has_used_trial: true;
|
has_used_trial: true;
|
||||||
|
@ -16,6 +16,7 @@ export enum OrgPermissionSubjects {
|
|||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
IncidentAccount = "incident-contact",
|
IncidentAccount = "incident-contact",
|
||||||
Sso = "sso",
|
Sso = "sso",
|
||||||
|
Scim = "scim",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity"
|
Identity = "identity"
|
||||||
@ -29,6 +30,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||||
@ -69,6 +71,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
|
@ -177,6 +177,8 @@ export const permissionServiceFactory = ({
|
|||||||
|
|
||||||
const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => {
|
const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => {
|
||||||
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
|
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
|
||||||
|
if (!serviceToken) throw new BadRequestError({ message: "Service token not found" });
|
||||||
|
|
||||||
if (serviceToken.projectId !== projectId)
|
if (serviceToken.projectId !== projectId)
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
message: "Failed to find service authorization for given project"
|
message: "Failed to find service authorization for given project"
|
||||||
|
@ -195,7 +195,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
updateQuery.certTag = certTag;
|
updateQuery.certTag = certTag;
|
||||||
}
|
}
|
||||||
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
||||||
await orgDAL.updateById(orgId, { authEnforced: false });
|
await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false });
|
||||||
|
|
||||||
return ssoConfig;
|
return ssoConfig;
|
||||||
};
|
};
|
||||||
@ -338,7 +338,8 @@ export const samlConfigServiceFactory = ({
|
|||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
authMethods: [AuthMethod.EMAIL]
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
isGhost: false
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
10
backend/src/ee/services/scim/scim-dal.ts
Normal file
10
backend/src/ee/services/scim/scim-dal.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
|
||||||
|
|
||||||
|
export const scimDALFactory = (db: TDbClient) => {
|
||||||
|
const scimTokenOrm = ormify(db, TableName.ScimToken);
|
||||||
|
return scimTokenOrm;
|
||||||
|
};
|
58
backend/src/ee/services/scim/scim-fns.ts
Normal file
58
backend/src/ee/services/scim/scim-fns.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { TListScimUsers, TScimUser } from "./scim-types";
|
||||||
|
|
||||||
|
export const buildScimUserList = ({
|
||||||
|
scimUsers,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
}: {
|
||||||
|
scimUsers: TScimUser[];
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}): TListScimUsers => {
|
||||||
|
return {
|
||||||
|
Resources: scimUsers,
|
||||||
|
itemsPerPage: limit,
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||||
|
startIndex: offset,
|
||||||
|
totalResults: scimUsers.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildScimUser = ({
|
||||||
|
userId,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
active
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
active: boolean;
|
||||||
|
}): TScimUser => {
|
||||||
|
return {
|
||||||
|
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||||
|
id: userId,
|
||||||
|
userName: email,
|
||||||
|
displayName: `${firstName} ${lastName}`,
|
||||||
|
name: {
|
||||||
|
givenName: firstName,
|
||||||
|
middleName: null,
|
||||||
|
familyName: lastName
|
||||||
|
},
|
||||||
|
emails: [
|
||||||
|
{
|
||||||
|
primary: true,
|
||||||
|
value: email,
|
||||||
|
type: "work"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
active,
|
||||||
|
groups: [],
|
||||||
|
meta: {
|
||||||
|
resourceType: "User",
|
||||||
|
location: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
431
backend/src/ee/services/scim/scim-service.ts
Normal file
431
backend/src/ee/services/scim/scim-service.ts
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||||
|
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { deleteOrgMembership } from "@app/services/org/org-fns";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { buildScimUser, buildScimUserList } from "./scim-fns";
|
||||||
|
import {
|
||||||
|
TCreateScimTokenDTO,
|
||||||
|
TCreateScimUserDTO,
|
||||||
|
TDeleteScimTokenDTO,
|
||||||
|
TGetScimUserDTO,
|
||||||
|
TListScimUsers,
|
||||||
|
TListScimUsersDTO,
|
||||||
|
TReplaceScimUserDTO,
|
||||||
|
TScimTokenJwtPayload,
|
||||||
|
TUpdateScimUserDTO
|
||||||
|
} from "./scim-types";
|
||||||
|
|
||||||
|
type TScimServiceFactoryDep = {
|
||||||
|
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
|
||||||
|
orgDAL: Pick<
|
||||||
|
TOrgDALFactory,
|
||||||
|
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
|
||||||
|
>;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||||
|
|
||||||
|
export const scimServiceFactory = ({
|
||||||
|
licenseService,
|
||||||
|
scimDAL,
|
||||||
|
userDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
permissionService,
|
||||||
|
smtpService
|
||||||
|
}: TScimServiceFactoryDep) => {
|
||||||
|
const createScimToken = async ({ actor, actorId, actorOrgId, orgId, description, ttlDays }: TCreateScimTokenDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create a SCIM token due to plan restriction. Upgrade plan to create a SCIM token."
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const scimTokenData = await scimDAL.create({
|
||||||
|
orgId,
|
||||||
|
description,
|
||||||
|
ttlDays
|
||||||
|
});
|
||||||
|
|
||||||
|
const scimToken = jwt.sign(
|
||||||
|
{
|
||||||
|
scimTokenId: scimTokenData.id,
|
||||||
|
authTokenType: AuthTokenType.SCIM_TOKEN
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
return { scimToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
const listScimTokens = async ({ actor, actorId, actorOrgId, orgId }: TOrgPermission) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get SCIM tokens due to plan restriction. Upgrade plan to get SCIM tokens."
|
||||||
|
});
|
||||||
|
|
||||||
|
const scimTokens = await scimDAL.find({ orgId });
|
||||||
|
return scimTokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorOrgId }: TDeleteScimTokenDTO) => {
|
||||||
|
let scimToken = await scimDAL.findById(scimTokenId);
|
||||||
|
if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, scimToken.orgId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(scimToken.orgId);
|
||||||
|
if (!plan.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete the SCIM token due to plan restriction. Upgrade plan to delete the SCIM token."
|
||||||
|
});
|
||||||
|
|
||||||
|
scimToken = await scimDAL.deleteById(scimTokenId);
|
||||||
|
|
||||||
|
return scimToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
// SCIM server endpoints
|
||||||
|
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
|
||||||
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
|
if (!org.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseFilter = (filterToParse: string | undefined) => {
|
||||||
|
if (!filterToParse) return {};
|
||||||
|
const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
|
||||||
|
|
||||||
|
let attributeName = parsedName;
|
||||||
|
if (parsedName === "userName") {
|
||||||
|
attributeName = "email";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [attributeName]: parsedValue };
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOpts = {
|
||||||
|
...(offset && { offset }),
|
||||||
|
...(limit && { limit })
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = await orgDAL.findMembership(
|
||||||
|
{
|
||||||
|
orgId,
|
||||||
|
...parseFilter(filter)
|
||||||
|
},
|
||||||
|
findOpts
|
||||||
|
);
|
||||||
|
|
||||||
|
const scimUsers = users.map(({ userId, firstName, lastName, email }) =>
|
||||||
|
buildScimUser({
|
||||||
|
userId: userId ?? "",
|
||||||
|
firstName: firstName ?? "",
|
||||||
|
lastName: lastName ?? "",
|
||||||
|
email,
|
||||||
|
active: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildScimUserList({
|
||||||
|
scimUsers,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => {
|
||||||
|
const [membership] = await orgDAL
|
||||||
|
.findMembership({
|
||||||
|
userId,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: membership.userId as string,
|
||||||
|
firstName: membership.firstName as string,
|
||||||
|
lastName: membership.lastName as string,
|
||||||
|
email: membership.email,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createScimUser = async ({ firstName, lastName, email, orgId }: TCreateScimUserDTO) => {
|
||||||
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
|
if (!org)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Organization not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!org.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
let user = await userDAL.findOne({
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
await userDAL.transaction(async (tx) => {
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx });
|
||||||
|
if (orgMembership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User already exists in the database",
|
||||||
|
status: 409
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orgMembership) {
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
orgId,
|
||||||
|
inviteEmail: email,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
user = await userDAL.transaction(async (tx) => {
|
||||||
|
const newUser = await userDAL.create(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
isGhost: false
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
orgId,
|
||||||
|
userId: newUser.id,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newUser;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.ScimUserProvisioned,
|
||||||
|
subjectLine: "Infisical organization invitation",
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
organizationName: org.name,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: user.id,
|
||||||
|
firstName: user.firstName as string,
|
||||||
|
lastName: user.lastName as string,
|
||||||
|
email: user.email,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||||
|
const [membership] = await orgDAL
|
||||||
|
.findMembership({
|
||||||
|
userId,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
operations.forEach((operation) => {
|
||||||
|
if (operation.op.toLowerCase() === "replace") {
|
||||||
|
if (operation.path === "active" && operation.value === "False") {
|
||||||
|
// azure scim op format
|
||||||
|
active = false;
|
||||||
|
} else if (typeof operation.value === "object" && operation.value.active === false) {
|
||||||
|
// okta scim op format
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
await deleteOrgMembership({
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
orgId: membership.orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: membership.userId as string,
|
||||||
|
firstName: membership.firstName as string,
|
||||||
|
lastName: membership.lastName as string,
|
||||||
|
email: membership.email,
|
||||||
|
active
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => {
|
||||||
|
const [membership] = await orgDAL
|
||||||
|
.findMembership({
|
||||||
|
userId,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
// tx
|
||||||
|
await deleteOrgMembership({
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
orgId: membership.orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: membership.userId as string,
|
||||||
|
firstName: membership.firstName as string,
|
||||||
|
lastName: membership.lastName as string,
|
||||||
|
email: membership.email,
|
||||||
|
active
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
|
||||||
|
const scimToken = await scimDAL.findById(token.scimTokenId);
|
||||||
|
if (!scimToken) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const { ttlDays, createdAt } = scimToken;
|
||||||
|
|
||||||
|
// ttl check
|
||||||
|
if (Number(ttlDays) > 0) {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const scimTokenCreatedAt = new Date(createdAt);
|
||||||
|
const ttlInMilliseconds = Number(scimToken.ttlDays) * 86400 * 1000;
|
||||||
|
const expirationDate = new Date(scimTokenCreatedAt.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
|
if (currentDate > expirationDate)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "The access token expired",
|
||||||
|
status: 401
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createScimToken,
|
||||||
|
listScimTokens,
|
||||||
|
deleteScimToken,
|
||||||
|
listScimUsers,
|
||||||
|
getScimUser,
|
||||||
|
createScimUser,
|
||||||
|
updateScimUser,
|
||||||
|
replaceScimUser,
|
||||||
|
fnValidateScimToken
|
||||||
|
};
|
||||||
|
};
|
87
backend/src/ee/services/scim/scim-types.ts
Normal file
87
backend/src/ee/services/scim/scim-types.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateScimTokenDTO = {
|
||||||
|
description: string;
|
||||||
|
ttlDays: number;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TDeleteScimTokenDTO = {
|
||||||
|
scimTokenId: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
// SCIM server endpoint types
|
||||||
|
|
||||||
|
export type TListScimUsersDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
filter?: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListScimUsers = {
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
|
||||||
|
totalResults: number;
|
||||||
|
Resources: TScimUser[];
|
||||||
|
itemsPerPage: number;
|
||||||
|
startIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetScimUserDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateScimUserDTO = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateScimUserDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
operations: {
|
||||||
|
op: string;
|
||||||
|
path?: string;
|
||||||
|
value?:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TReplaceScimUserDTO = {
|
||||||
|
userId: string;
|
||||||
|
active: boolean;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TScimTokenJwtPayload = {
|
||||||
|
scimTokenId: string;
|
||||||
|
authTokenType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TScimUser = {
|
||||||
|
schemas: string[];
|
||||||
|
id: string;
|
||||||
|
userName: string;
|
||||||
|
displayName: string;
|
||||||
|
name: {
|
||||||
|
givenName: string;
|
||||||
|
middleName: null;
|
||||||
|
familyName: string;
|
||||||
|
};
|
||||||
|
emails: {
|
||||||
|
primary: boolean;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
}[];
|
||||||
|
active: boolean;
|
||||||
|
groups: string[];
|
||||||
|
meta: {
|
||||||
|
resourceType: string;
|
||||||
|
location: null;
|
||||||
|
};
|
||||||
|
};
|
@ -1,8 +1,13 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas";
|
import {
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
SecretApprovalRequestsSecretsSchema,
|
||||||
|
TableName,
|
||||||
|
TSecretApprovalRequestsSecrets,
|
||||||
|
TSecretTags
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
|
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
|
||||||
@ -11,6 +16,35 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
|
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
|
||||||
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
|
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
|
||||||
|
|
||||||
|
const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const existingApprovalSecrets = await secretApprovalRequestSecretOrm.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: data.map((el) => el.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingApprovalSecrets.length !== data.length) {
|
||||||
|
throw new BadRequestError({ message: "Some of the secret approvals do not exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) return [];
|
||||||
|
|
||||||
|
const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret)
|
||||||
|
.insert(data)
|
||||||
|
.onConflict("id") // this will cause a conflict then merge the data
|
||||||
|
.merge() // Merge the data with the existing data
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
return updatedApprovalSecrets;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findByRequestId = async (requestId: string, tx?: Knex) => {
|
const findByRequestId = async (requestId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db)({
|
const doc = await (tx || db)({
|
||||||
@ -190,6 +224,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
...secretApprovalRequestSecretOrm,
|
...secretApprovalRequestSecretOrm,
|
||||||
findByRequestId,
|
findByRequestId,
|
||||||
|
bulkUpdateNoVersionIncrement,
|
||||||
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
|
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
|||||||
import { groupBy, pick, unique } from "@app/lib/fn";
|
import { groupBy, pick, unique } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
|
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
|
||||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||||
@ -47,6 +48,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||||
secretService: Pick<
|
secretService: Pick<
|
||||||
TSecretServiceFactory,
|
TSecretServiceFactory,
|
||||||
| "fnSecretBulkInsert"
|
| "fnSecretBulkInsert"
|
||||||
@ -67,6 +69,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretApprovalRequestReviewerDAL,
|
secretApprovalRequestReviewerDAL,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
secretService,
|
secretService,
|
||||||
@ -434,6 +437,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
|
@ -8,6 +8,9 @@ import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
|||||||
|
|
||||||
import { getConfig } from "../config/env";
|
import { getConfig } from "../config/env";
|
||||||
|
|
||||||
|
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
||||||
|
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
||||||
|
|
||||||
export type TDecryptSymmetricInput = {
|
export type TDecryptSymmetricInput = {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
export {
|
export {
|
||||||
buildSecretBlindIndexFromName,
|
buildSecretBlindIndexFromName,
|
||||||
createSecretBlindIndex,
|
createSecretBlindIndex,
|
||||||
|
decodeBase64,
|
||||||
decryptAsymmetric,
|
decryptAsymmetric,
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
decryptSymmetric128BitHexKeyUTF8,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
|
encodeBase64,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
generateAsymmetricKeyPair
|
generateAsymmetricKeyPair
|
||||||
} from "./encryption";
|
} from "./encryption";
|
||||||
|
export {
|
||||||
|
decryptIntegrationAuths,
|
||||||
|
decryptSecretApprovals,
|
||||||
|
decryptSecrets,
|
||||||
|
decryptSecretVersions
|
||||||
|
} from "./secret-encryption";
|
||||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||||
|
293
backend/src/lib/crypto/secret-encryption.ts
Normal file
293
backend/src/lib/crypto/secret-encryption.ts
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
IntegrationAuthsSchema,
|
||||||
|
SecretApprovalRequestsSecretsSchema,
|
||||||
|
SecretsSchema,
|
||||||
|
SecretVersionsSchema,
|
||||||
|
TIntegrationAuths,
|
||||||
|
TProjectKeys,
|
||||||
|
TSecretApprovalRequestsSecrets,
|
||||||
|
TSecrets,
|
||||||
|
TSecretVersions
|
||||||
|
} from "../../db/schemas";
|
||||||
|
import { decryptAsymmetric } from "./encryption";
|
||||||
|
|
||||||
|
const DecryptedValuesSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
secretKey: z.string(),
|
||||||
|
secretValue: z.string(),
|
||||||
|
secretComment: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const DecryptedSecretSchema = z.object({
|
||||||
|
decrypted: DecryptedValuesSchema,
|
||||||
|
original: SecretsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const DecryptedIntegrationAuthsSchema = z.object({
|
||||||
|
decrypted: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
access: z.string(),
|
||||||
|
accessId: z.string(),
|
||||||
|
refresh: z.string()
|
||||||
|
}),
|
||||||
|
original: IntegrationAuthsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const DecryptedSecretVersionsSchema = z.object({
|
||||||
|
decrypted: DecryptedValuesSchema,
|
||||||
|
original: SecretVersionsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const DecryptedSecretApprovalsSchema = z.object({
|
||||||
|
decrypted: DecryptedValuesSchema,
|
||||||
|
original: SecretApprovalRequestsSecretsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
type DecryptedSecret = z.infer<typeof DecryptedSecretSchema>;
|
||||||
|
type DecryptedSecretVersions = z.infer<typeof DecryptedSecretVersionsSchema>;
|
||||||
|
type DecryptedSecretApprovals = z.infer<typeof DecryptedSecretApprovalsSchema>;
|
||||||
|
type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>;
|
||||||
|
|
||||||
|
type TLatestKey = TProjectKeys & {
|
||||||
|
sender: {
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptCipher = ({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
key
|
||||||
|
}: {
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
key: string | Buffer;
|
||||||
|
}) => {
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64"));
|
||||||
|
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||||
|
|
||||||
|
let cleartext = decipher.update(ciphertext, "base64", "utf8");
|
||||||
|
cleartext += decipher.final("utf8");
|
||||||
|
|
||||||
|
return cleartext;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: string }>, key: string | Buffer) => {
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
for (const { ciphertext, iv, tag } of data) {
|
||||||
|
if (!ciphertext || !iv || !tag) {
|
||||||
|
results.push("");
|
||||||
|
} else {
|
||||||
|
results.push(decryptCipher({ ciphertext, iv, tag, key }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
|
||||||
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecrets: DecryptedSecret[] = [];
|
||||||
|
|
||||||
|
encryptedSecrets.forEach((encSecret) => {
|
||||||
|
const [secretKey, secretValue, secretComment] = getDecryptedValues(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
|
iv: encSecret.secretValueIV,
|
||||||
|
tag: encSecret.secretValueTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encSecret.secretCommentCiphertext || "",
|
||||||
|
iv: encSecret.secretCommentIV || "",
|
||||||
|
tag: encSecret.secretCommentTag || ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptedSecret: DecryptedSecret = {
|
||||||
|
decrypted: {
|
||||||
|
secretKey,
|
||||||
|
secretValue,
|
||||||
|
secretComment,
|
||||||
|
id: encSecret.id
|
||||||
|
},
|
||||||
|
original: encSecret
|
||||||
|
};
|
||||||
|
|
||||||
|
decryptedSecrets.push(DecryptedSecretSchema.parse(decryptedSecret));
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptedSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSecretVersions = (
|
||||||
|
encryptedSecretVersions: TSecretVersions[],
|
||||||
|
privateKey: string,
|
||||||
|
latestKey: TLatestKey
|
||||||
|
) => {
|
||||||
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecrets: DecryptedSecretVersions[] = [];
|
||||||
|
|
||||||
|
encryptedSecretVersions.forEach((encSecret) => {
|
||||||
|
const [secretKey, secretValue, secretComment] = getDecryptedValues(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
|
iv: encSecret.secretValueIV,
|
||||||
|
tag: encSecret.secretValueTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encSecret.secretCommentCiphertext || "",
|
||||||
|
iv: encSecret.secretCommentIV || "",
|
||||||
|
tag: encSecret.secretCommentTag || ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptedSecret: DecryptedSecretVersions = {
|
||||||
|
decrypted: {
|
||||||
|
secretKey,
|
||||||
|
secretValue,
|
||||||
|
secretComment,
|
||||||
|
id: encSecret.id
|
||||||
|
},
|
||||||
|
original: encSecret
|
||||||
|
};
|
||||||
|
|
||||||
|
decryptedSecrets.push(DecryptedSecretVersionsSchema.parse(decryptedSecret));
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptedSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSecretApprovals = (
|
||||||
|
encryptedSecretApprovals: TSecretApprovalRequestsSecrets[],
|
||||||
|
privateKey: string,
|
||||||
|
latestKey: TLatestKey
|
||||||
|
) => {
|
||||||
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecrets: DecryptedSecretApprovals[] = [];
|
||||||
|
|
||||||
|
encryptedSecretApprovals.forEach((encApproval) => {
|
||||||
|
const [secretKey, secretValue, secretComment] = getDecryptedValues(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ciphertext: encApproval.secretKeyCiphertext,
|
||||||
|
iv: encApproval.secretKeyIV,
|
||||||
|
tag: encApproval.secretKeyTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encApproval.secretValueCiphertext,
|
||||||
|
iv: encApproval.secretValueIV,
|
||||||
|
tag: encApproval.secretValueTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encApproval.secretCommentCiphertext || "",
|
||||||
|
iv: encApproval.secretCommentIV || "",
|
||||||
|
tag: encApproval.secretCommentTag || ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptedSecret: DecryptedSecretApprovals = {
|
||||||
|
decrypted: {
|
||||||
|
secretKey,
|
||||||
|
secretValue,
|
||||||
|
secretComment,
|
||||||
|
id: encApproval.id
|
||||||
|
},
|
||||||
|
original: encApproval
|
||||||
|
};
|
||||||
|
|
||||||
|
decryptedSecrets.push(DecryptedSecretApprovalsSchema.parse(decryptedSecret));
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptedSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptIntegrationAuths = (
|
||||||
|
encryptedIntegrationAuths: TIntegrationAuths[],
|
||||||
|
privateKey: string,
|
||||||
|
latestKey: TLatestKey
|
||||||
|
) => {
|
||||||
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedIntegrationAuths: DecryptedIntegrationAuths[] = [];
|
||||||
|
|
||||||
|
encryptedIntegrationAuths.forEach((encAuth) => {
|
||||||
|
const [access, accessId, refresh] = getDecryptedValues(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ciphertext: encAuth.accessCiphertext || "",
|
||||||
|
iv: encAuth.accessIV || "",
|
||||||
|
tag: encAuth.accessTag || ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encAuth.accessIdCiphertext || "",
|
||||||
|
iv: encAuth.accessIdIV || "",
|
||||||
|
tag: encAuth.accessIdTag || ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ciphertext: encAuth.refreshCiphertext || "",
|
||||||
|
iv: encAuth.refreshIV || "",
|
||||||
|
tag: encAuth.refreshTag || ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
decryptedIntegrationAuths.push({
|
||||||
|
decrypted: {
|
||||||
|
id: encAuth.id,
|
||||||
|
access,
|
||||||
|
accessId,
|
||||||
|
refresh
|
||||||
|
},
|
||||||
|
original: encAuth
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptedIntegrationAuths;
|
||||||
|
};
|
@ -1,4 +1,12 @@
|
|||||||
|
import argon2 from "argon2";
|
||||||
|
import crypto from "crypto";
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
import tweetnacl from "tweetnacl-util";
|
||||||
|
|
||||||
|
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
|
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
||||||
|
|
||||||
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
@ -24,3 +32,99 @@ export const srpCheckClientProof = async (
|
|||||||
server.setClientPublicKey(clientPublicKey);
|
server.setClientPublicKey(clientPublicKey);
|
||||||
return server.checkClientProof(clientProof);
|
return server.checkClientProof(clientProof);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
|
const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array);
|
||||||
|
const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array);
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const client = new jsrp.client();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
client.init({ username: email, password }, () => resolve(null));
|
||||||
|
});
|
||||||
|
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>((resolve, reject) => {
|
||||||
|
client.createVerifier((err, res) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
return resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const derivedKey = await argon2.hash(password, {
|
||||||
|
salt: Buffer.from(salt),
|
||||||
|
memoryCost: 65536,
|
||||||
|
timeCost: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLength: 32,
|
||||||
|
type: argon2.argon2id,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
// create encrypted private key by encrypting the private
|
||||||
|
// key with the symmetric key [key]
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = encryptSymmetric(privateKey, key.toString("base64"));
|
||||||
|
|
||||||
|
// create the protected key by encrypting the symmetric key
|
||||||
|
// [key] with the derived key
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
protectedKey,
|
||||||
|
plainPrivateKey: privateKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
||||||
|
const derivedKey = await argon2.hash(password, {
|
||||||
|
salt: Buffer.from(user.salt),
|
||||||
|
memoryCost: 65536,
|
||||||
|
timeCost: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLength: 32,
|
||||||
|
type: argon2.argon2id,
|
||||||
|
raw: true
|
||||||
|
});
|
||||||
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
|
const key = decryptSymmetric({
|
||||||
|
ciphertext: user.protectedKey!,
|
||||||
|
iv: user.protectedKeyIV!,
|
||||||
|
tag: user.protectedKeyTag!,
|
||||||
|
key: derivedKey.toString("base64")
|
||||||
|
});
|
||||||
|
const privateKey = decryptSymmetric({
|
||||||
|
ciphertext: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
return privateKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
|
||||||
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
|
||||||
|
return { nonce, ciphertext };
|
||||||
|
};
|
||||||
|
@ -58,3 +58,35 @@ export class BadRequestError extends Error {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ScimRequestError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
schemas: string[];
|
||||||
|
|
||||||
|
detail: string;
|
||||||
|
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
name,
|
||||||
|
error,
|
||||||
|
detail,
|
||||||
|
status
|
||||||
|
}: {
|
||||||
|
message?: string;
|
||||||
|
name?: string;
|
||||||
|
error?: unknown;
|
||||||
|
detail: string;
|
||||||
|
status: number;
|
||||||
|
}) {
|
||||||
|
super(detail ?? "The request is invalid");
|
||||||
|
this.name = name || "ScimRequestError";
|
||||||
|
this.schemas = ["urn:ietf:params:scim:api:messages:2.0:Error"];
|
||||||
|
this.error = error;
|
||||||
|
this.detail = detail;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
||||||
import Redis from "ioredis";
|
import Redis from "ioredis";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import {
|
||||||
TScanFullRepoEventPayload,
|
TScanFullRepoEventPayload,
|
||||||
@ -15,7 +16,8 @@ export enum QueueName {
|
|||||||
IntegrationSync = "sync-integrations",
|
IntegrationSync = "sync-integrations",
|
||||||
SecretWebhook = "secret-webhook",
|
SecretWebhook = "secret-webhook",
|
||||||
SecretFullRepoScan = "secret-full-repo-scan",
|
SecretFullRepoScan = "secret-full-repo-scan",
|
||||||
SecretPushEventScan = "secret-push-event-scan"
|
SecretPushEventScan = "secret-push-event-scan",
|
||||||
|
UpgradeProjectToGhost = "upgrade-project-to-ghost"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@ -25,7 +27,8 @@ export enum QueueJobs {
|
|||||||
AuditLogPrune = "audit-log-prune-job",
|
AuditLogPrune = "audit-log-prune-job",
|
||||||
SecWebhook = "secret-webhook-trigger",
|
SecWebhook = "secret-webhook-trigger",
|
||||||
IntegrationSync = "secret-integration-pull",
|
IntegrationSync = "secret-integration-pull",
|
||||||
SecretScan = "secret-scan"
|
SecretScan = "secret-scan",
|
||||||
|
UpgradeProjectToGhost = "upgrade-project-to-ghost-job"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@ -64,6 +67,20 @@ export type TQueueJobTypes = {
|
|||||||
payload: TScanFullRepoEventPayload;
|
payload: TScanFullRepoEventPayload;
|
||||||
};
|
};
|
||||||
[QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload };
|
[QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload };
|
||||||
|
|
||||||
|
[QueueName.UpgradeProjectToGhost]: {
|
||||||
|
name: QueueJobs.UpgradeProjectToGhost;
|
||||||
|
payload: {
|
||||||
|
projectId: string;
|
||||||
|
startedByUserId: string;
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
encryptedKey: string;
|
||||||
|
encryptedKeyIv: string;
|
||||||
|
encryptedKeyTag: string;
|
||||||
|
keyEncoding: SecretKeyEncoding;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
17
backend/src/server/lib/telemetry.ts
Normal file
17
backend/src/server/lib/telemetry.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { FastifyRequest } from "fastify";
|
||||||
|
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
// this is a unique id for sending posthog event
|
||||||
|
export const getTelemetryDistinctId = (req: FastifyRequest) => {
|
||||||
|
if (req.auth.actor === ActorType.USER) {
|
||||||
|
return req.auth.user.email;
|
||||||
|
}
|
||||||
|
if (req.auth.actor === ActorType.IDENTITY) {
|
||||||
|
return `identity-${req.auth.identityId}`;
|
||||||
|
}
|
||||||
|
if (req.auth.actor === ActorType.SERVICE) {
|
||||||
|
return req.auth.serviceToken.createdByEmail || `service-token-null-creator-${req.auth.serviceTokenId}`; // when user gets removed from system
|
||||||
|
}
|
||||||
|
return "unknown-auth-data";
|
||||||
|
};
|
@ -63,6 +63,11 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
|
|||||||
identityId: req.auth.identityId
|
identityId: req.auth.identityId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
|
||||||
|
payload.actor = {
|
||||||
|
type: ActorType.SCIM_CLIENT,
|
||||||
|
metadata: {}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new BadRequestError({ message: "Missing logic for other actor" });
|
throw new BadRequestError({ message: "Missing logic for other actor" });
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import fp from "fastify-plugin";
|
|||||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
|
||||||
import { TServiceTokens, TUsers } from "@app/db/schemas";
|
import { TServiceTokens, TUsers } from "@app/db/schemas";
|
||||||
|
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
@ -26,7 +27,7 @@ export type TAuthMode =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
authMode: AuthMode.SERVICE_TOKEN;
|
authMode: AuthMode.SERVICE_TOKEN;
|
||||||
serviceToken: TServiceTokens;
|
serviceToken: TServiceTokens & { createdByEmail: string };
|
||||||
actor: ActorType.SERVICE;
|
actor: ActorType.SERVICE;
|
||||||
serviceTokenId: string;
|
serviceTokenId: string;
|
||||||
}
|
}
|
||||||
@ -35,6 +36,12 @@ export type TAuthMode =
|
|||||||
actor: ActorType.IDENTITY;
|
actor: ActorType.IDENTITY;
|
||||||
identityId: string;
|
identityId: string;
|
||||||
identityName: string;
|
identityName: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
authMode: AuthMode.SCIM_TOKEN;
|
||||||
|
actor: ActorType.SCIM_CLIENT;
|
||||||
|
scimTokenId: string;
|
||||||
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||||
@ -55,6 +62,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
|
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
|
||||||
|
|
||||||
switch (decodedToken.authTokenType) {
|
switch (decodedToken.authTokenType) {
|
||||||
case AuthTokenType.ACCESS_TOKEN:
|
case AuthTokenType.ACCESS_TOKEN:
|
||||||
return {
|
return {
|
||||||
@ -70,6 +78,12 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
|||||||
token: decodedToken as TIdentityAccessTokenJwtPayload,
|
token: decodedToken as TIdentityAccessTokenJwtPayload,
|
||||||
actor: ActorType.IDENTITY
|
actor: ActorType.IDENTITY
|
||||||
} as const;
|
} as const;
|
||||||
|
case AuthTokenType.SCIM_TOKEN:
|
||||||
|
return {
|
||||||
|
authMode: AuthMode.SCIM_TOKEN,
|
||||||
|
token: decodedToken as TScimTokenJwtPayload,
|
||||||
|
actor: ActorType.SCIM_CLIENT
|
||||||
|
} as const;
|
||||||
default:
|
default:
|
||||||
return { authMode: null, token: null } as const;
|
return { authMode: null, token: null } as const;
|
||||||
}
|
}
|
||||||
@ -113,6 +127,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
|
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AuthMode.SCIM_TOKEN: {
|
||||||
|
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
|
||||||
|
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId };
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new UnauthorizedError({ name: "Unknown token strategy" });
|
throw new UnauthorizedError({ name: "Unknown token strategy" });
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ export const injectPermission = fp(async (server) => {
|
|||||||
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
|
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
|
||||||
} else if (req.auth.actor === ActorType.SERVICE) {
|
} else if (req.auth.actor === ActorType.SERVICE) {
|
||||||
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
|
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
|
||||||
|
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
|
||||||
|
req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,13 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { BadRequestError, DatabaseError, InternalServerError, UnauthorizedError } from "@app/lib/errors";
|
import {
|
||||||
|
BadRequestError,
|
||||||
|
DatabaseError,
|
||||||
|
InternalServerError,
|
||||||
|
ScimRequestError,
|
||||||
|
UnauthorizedError
|
||||||
|
} from "@app/lib/errors";
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
@ -21,6 +27,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
error: "PermissionDenied",
|
error: "PermissionDenied",
|
||||||
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
||||||
});
|
});
|
||||||
|
} else if (error instanceof ScimRequestError) {
|
||||||
|
void res.status(error.status).send({
|
||||||
|
schemas: error.schemas,
|
||||||
|
status: error.status,
|
||||||
|
detail: error.detail
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
void res.send(error);
|
void res.send(error);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal
|
|||||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
|
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
|
import { scimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||||
import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
|
import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
|
||||||
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
|
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
|
||||||
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
@ -63,6 +65,7 @@ import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
|||||||
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@ -155,6 +158,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const auditLogDAL = auditLogDALFactory(db);
|
const auditLogDAL = auditLogDALFactory(db);
|
||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
|
const scimDAL = scimDALFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
@ -188,6 +192,7 @@ export const registerRoutes = async (
|
|||||||
trustedIpDAL,
|
trustedIpDAL,
|
||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const auditLogQueue = auditLogQueueServiceFactory({
|
const auditLogQueue = auditLogQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
@ -210,6 +215,16 @@ export const registerRoutes = async (
|
|||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
const scimService = scimServiceFactory({
|
||||||
|
licenseService,
|
||||||
|
scimDAL,
|
||||||
|
userDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
permissionService,
|
||||||
|
smtpService
|
||||||
|
});
|
||||||
|
|
||||||
const telemetryService = telemetryServiceFactory();
|
const telemetryService = telemetryServiceFactory();
|
||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||||
@ -266,19 +281,13 @@ export const registerRoutes = async (
|
|||||||
secretScanningDAL,
|
secretScanningDAL,
|
||||||
secretScanningQueue
|
secretScanningQueue
|
||||||
});
|
});
|
||||||
const projectService = projectServiceFactory({
|
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
||||||
permissionService,
|
|
||||||
projectDAL,
|
|
||||||
secretBlindIndexDAL,
|
|
||||||
projectEnvDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
folderDAL,
|
|
||||||
licenseService
|
|
||||||
});
|
|
||||||
const projectMembershipService = projectMembershipServiceFactory({
|
const projectMembershipService = projectMembershipServiceFactory({
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
@ -286,6 +295,46 @@ export const registerRoutes = async (
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
const projectKeyService = projectKeyServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectQueueService = projectQueueFactory({
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
projectDAL,
|
||||||
|
orgDAL,
|
||||||
|
integrationAuthDAL,
|
||||||
|
orgService,
|
||||||
|
projectEnvDAL,
|
||||||
|
userDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL: sarSecretDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectService = projectServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectDAL,
|
||||||
|
projectQueue: projectQueueService,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
identityProjectDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
userDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
orgService,
|
||||||
|
projectMembershipDAL,
|
||||||
|
folderDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@ -293,11 +342,7 @@ export const registerRoutes = async (
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
});
|
});
|
||||||
const projectKeyService = projectKeyServiceFactory({
|
|
||||||
permissionService,
|
|
||||||
projectKeyDAL,
|
|
||||||
projectMembershipDAL
|
|
||||||
});
|
|
||||||
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
|
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
|
||||||
|
|
||||||
const snapshotService = secretSnapshotServiceFactory({
|
const snapshotService = secretSnapshotServiceFactory({
|
||||||
@ -332,9 +377,9 @@ export const registerRoutes = async (
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
|
projectDAL,
|
||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL });
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
integrationAuthDAL,
|
integrationAuthDAL,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
@ -368,6 +413,7 @@ export const registerRoutes = async (
|
|||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
projectDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
@ -381,6 +427,7 @@ export const registerRoutes = async (
|
|||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretApprovalRequestSecretDAL: sarSecretDAL,
|
secretApprovalRequestSecretDAL: sarSecretDAL,
|
||||||
secretApprovalRequestReviewerDAL: sarReviewerDAL,
|
secretApprovalRequestReviewerDAL: sarReviewerDAL,
|
||||||
|
projectDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
@ -486,6 +533,7 @@ export const registerRoutes = async (
|
|||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
trustedIp: trustedIpService,
|
trustedIp: trustedIpService,
|
||||||
|
scim: scimService,
|
||||||
secretBlindIndex: secretBlindIndexService,
|
secretBlindIndex: secretBlindIndexService,
|
||||||
telemetry: telemetryService
|
telemetry: telemetryService
|
||||||
});
|
});
|
||||||
@ -537,4 +585,8 @@ export const registerRoutes = async (
|
|||||||
);
|
);
|
||||||
await server.register(registerV2Routes, { prefix: "/api/v2" });
|
await server.register(registerV2Routes, { prefix: "/api/v2" });
|
||||||
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
||||||
|
|
||||||
|
server.addHook("onClose", async () => {
|
||||||
|
await telemetryService.flushAll();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
@ -31,7 +31,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
allowSignUp: z.boolean().optional()
|
allowSignUp: z.boolean().optional(),
|
||||||
|
allowedSignUpDomain: z.string().optional().nullable()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -72,6 +73,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
user: UsersSchema,
|
user: UsersSchema,
|
||||||
|
organization: OrganizationsSchema,
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
new: z.string()
|
new: z.string()
|
||||||
})
|
})
|
||||||
@ -82,7 +84,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
if (serverCfg.initialized)
|
if (serverCfg.initialized)
|
||||||
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
|
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
|
||||||
const { user, token } = await server.services.superAdmin.adminSignUp({
|
const { user, token, organization } = await server.services.superAdmin.adminSignUp({
|
||||||
...req.body,
|
...req.body,
|
||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent: req.headers["user-agent"] || ""
|
userAgent: req.headers["user-agent"] || ""
|
||||||
@ -109,6 +111,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
message: "Successfully set up admin account",
|
message: "Successfully set up admin account",
|
||||||
user: user.user,
|
user: user.user,
|
||||||
token: token.access,
|
token: token.access,
|
||||||
|
organization,
|
||||||
new: "123"
|
new: "123"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,10 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas";
|
import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@ -49,6 +51,17 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.MachineIdentityCreated,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
name: identity.name,
|
||||||
|
identityId: identity.id,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { identity };
|
return { identity };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -48,6 +48,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await projectRouter.register(registerProjectMembershipRouter);
|
await projectRouter.register(registerProjectMembershipRouter);
|
||||||
await projectRouter.register(registerSecretTagRouter);
|
await projectRouter.register(registerSecretTagRouter);
|
||||||
},
|
},
|
||||||
|
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3,8 +3,10 @@ import { z } from "zod";
|
|||||||
import { IntegrationsSchema } from "@app/db/schemas";
|
import { IntegrationsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { removeTrailingSlash, shake } from "@app/lib/fn";
|
import { removeTrailingSlash, shake } from "@app/lib/fn";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@ -53,28 +55,40 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.body
|
...req.body
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createIntegrationEventProperty = shake({
|
||||||
|
integrationId: integration.id.toString(),
|
||||||
|
integration: integration.integration,
|
||||||
|
environment: req.body.sourceEnvironment,
|
||||||
|
secretPath: req.body.secretPath,
|
||||||
|
url: integration.url,
|
||||||
|
app: integration.app,
|
||||||
|
appId: integration.appId,
|
||||||
|
targetEnvironment: integration.targetEnvironment,
|
||||||
|
targetEnvironmentId: integration.targetEnvironmentId,
|
||||||
|
targetService: integration.targetService,
|
||||||
|
targetServiceId: integration.targetServiceId,
|
||||||
|
path: integration.path,
|
||||||
|
region: integration.region
|
||||||
|
}) as TIntegrationCreatedEvent["properties"];
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: integrationAuth.projectId,
|
projectId: integrationAuth.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.CREATE_INTEGRATION,
|
type: EventType.CREATE_INTEGRATION,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
metadata: shake({
|
metadata: createIntegrationEventProperty
|
||||||
integrationId: integration.id.toString(),
|
}
|
||||||
integration: integration.integration,
|
});
|
||||||
environment: req.body.sourceEnvironment,
|
|
||||||
secretPath: req.body.secretPath,
|
server.services.telemetry.sendPostHogEvents({
|
||||||
url: integration.url,
|
event: PostHogEventTypes.IntegrationCreated,
|
||||||
app: integration.app,
|
distinctId: getTelemetryDistinctId(req),
|
||||||
appId: integration.appId,
|
properties: {
|
||||||
targetEnvironment: integration.targetEnvironment,
|
...createIntegrationEventProperty,
|
||||||
targetEnvironmentId: integration.targetEnvironmentId,
|
projectId: integrationAuth.projectId,
|
||||||
targetService: integration.targetService,
|
...req.auditLogInfo
|
||||||
targetServiceId: integration.targetServiceId,
|
|
||||||
path: integration.path,
|
|
||||||
region: integration.region
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}) as any
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { integration };
|
return { integration };
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { UsersSchema } from "@app/db/schemas";
|
import { UsersSchema } from "@app/db/schemas";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@ -30,6 +32,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.UserOrgInvitation,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
inviteeEmail: req.body.inviteeEmail,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
completeInviteLink,
|
completeInviteLink,
|
||||||
message: `Send an invite link to ${req.body.inviteeEmail}`
|
message: `Send an invite link to ${req.body.inviteeEmail}`
|
||||||
|
@ -93,7 +93,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
.trim()
|
.trim()
|
||||||
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
||||||
.optional(),
|
.optional(),
|
||||||
authEnforced: z.boolean().optional()
|
authEnforced: z.boolean().optional(),
|
||||||
|
scimEnabled: z.boolean().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrgMembershipsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import {
|
||||||
|
OrgMembershipsSchema,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectMembershipsSchema,
|
||||||
|
UserEncryptionKeysSchema,
|
||||||
|
UsersSchema
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -80,7 +86,10 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
members: req.body.members
|
members: req.body.members.map((member) => ({
|
||||||
|
...member,
|
||||||
|
projectRole: ProjectMembershipRole.Member
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
@ -2,13 +2,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
IntegrationsSchema,
|
IntegrationsSchema,
|
||||||
ProjectKeysSchema,
|
|
||||||
ProjectMembershipsSchema,
|
ProjectMembershipsSchema,
|
||||||
ProjectsSchema,
|
ProjectsSchema,
|
||||||
UserEncryptionKeysSchema,
|
UserEncryptionKeysSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@ -119,7 +117,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspace = await server.services.project.getAProject({
|
const workspace = await server.services.project.getAProject({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@ -171,7 +169,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspace = await server.services.project.deleteProject({
|
const workspace = await server.services.project.deleteProject({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@ -216,6 +214,41 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim().optional(),
|
||||||
|
autoCapitalization: z.boolean().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspace: ProjectsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.updateProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
update: {
|
||||||
|
name: req.body.name,
|
||||||
|
autoCapitalization: req.body.autoCapitalization
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId/auto-capitalization",
|
url: "/:workspaceId/auto-capitalization",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -249,48 +282,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
|
||||||
url: "/:workspaceId/invite-signup",
|
|
||||||
method: "POST",
|
|
||||||
schema: {
|
|
||||||
params: z.object({
|
|
||||||
workspaceId: z.string().trim()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
email: z.string().trim()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
invitee: UsersSchema,
|
|
||||||
latestKey: ProjectKeysSchema.optional()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const { invitee, latestKey } = await server.services.projectMembership.inviteUserToProject({
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
projectId: req.params.workspaceId,
|
|
||||||
email: req.body.email
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
|
||||||
...req.auditLogInfo,
|
|
||||||
projectId: req.params.workspaceId,
|
|
||||||
event: {
|
|
||||||
type: EventType.ADD_WORKSPACE_MEMBER,
|
|
||||||
metadata: {
|
|
||||||
userId: invitee.id,
|
|
||||||
email: invitee.email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { invitee, latestKey };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId/integrations",
|
url: "/:workspaceId/integrations",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -18,7 +18,6 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { fetchGithubEmails } from "@app/lib/requests/github";
|
import { fetchGithubEmails } from "@app/lib/requests/github";
|
||||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
|
||||||
|
|
||||||
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@ -42,7 +41,6 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
async (req, _accessToken, _refreshToken, profile, cb) => {
|
async (req, _accessToken, _refreshToken, profile, cb) => {
|
||||||
try {
|
try {
|
||||||
const email = profile?.emails?.[0]?.value;
|
const email = profile?.emails?.[0]?.value;
|
||||||
const serverCfg = await getServerCfg();
|
|
||||||
if (!email)
|
if (!email)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Email not found",
|
message: "Email not found",
|
||||||
@ -54,8 +52,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
firstName: profile?.name?.givenName || "",
|
firstName: profile?.name?.givenName || "",
|
||||||
lastName: profile?.name?.familyName || "",
|
lastName: profile?.name?.familyName || "",
|
||||||
authMethod: AuthMethod.GOOGLE,
|
authMethod: AuthMethod.GOOGLE,
|
||||||
callbackPort: req.query.state as string,
|
callbackPort: req.query.state as string
|
||||||
isSignupAllowed: Boolean(serverCfg.allowSignUp)
|
|
||||||
});
|
});
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -84,14 +81,12 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
try {
|
try {
|
||||||
const ghEmails = await fetchGithubEmails(accessToken);
|
const ghEmails = await fetchGithubEmails(accessToken);
|
||||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||||
const serverCfg = await getServerCfg();
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile.displayName,
|
firstName: profile.displayName,
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethod: AuthMethod.GITHUB,
|
authMethod: AuthMethod.GITHUB,
|
||||||
callbackPort: req.query.state as string,
|
callbackPort: req.query.state as string
|
||||||
isSignupAllowed: Boolean(serverCfg.allowSignUp)
|
|
||||||
});
|
});
|
||||||
return cb(null, { isUserCompleted, providerAuthToken });
|
return cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -120,14 +115,12 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
||||||
try {
|
try {
|
||||||
const email = profile.emails[0].value;
|
const email = profile.emails[0].value;
|
||||||
const serverCfg = await getServerCfg();
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile.displayName,
|
firstName: profile.displayName,
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethod: AuthMethod.GITLAB,
|
authMethod: AuthMethod.GITLAB,
|
||||||
callbackPort: req.query.state as string,
|
callbackPort: req.query.state as string
|
||||||
isSignupAllowed: Boolean(serverCfg.allowSignUp)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb(null, { isUserCompleted, providerAuthToken });
|
return cb(null, { isUserCompleted, providerAuthToken });
|
||||||
|
@ -2,6 +2,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
|
|||||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||||
import { registerMfaRouter } from "./mfa-router";
|
import { registerMfaRouter } from "./mfa-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerServiceTokenRouter } from "./service-token-router";
|
import { registerServiceTokenRouter } from "./service-token-router";
|
||||||
import { registerUserRouter } from "./user-router";
|
import { registerUserRouter } from "./user-router";
|
||||||
@ -21,6 +22,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
|
|||||||
async (projectServer) => {
|
async (projectServer) => {
|
||||||
await projectServer.register(registerProjectRouter);
|
await projectServer.register(registerProjectRouter);
|
||||||
await projectServer.register(registerIdentityProjectRouter);
|
await projectServer.register(registerIdentityProjectRouter);
|
||||||
|
await projectServer.register(registerProjectMembershipRouter);
|
||||||
},
|
},
|
||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
95
backend/src/server/routes/v2/project-membership-router.ts
Normal file
95
backend/src/server/routes/v2/project-membership-router.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:projectId/memberships",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().describe("The ID of the project.")
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
emails: z.string().email().array().describe("Emails of the users to add to the project.")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
memberships: ProjectMembershipsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
emails: req.body.emails
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
|
||||||
|
metadata: memberships.map(({ userId, id }) => ({
|
||||||
|
userId: userId || "",
|
||||||
|
membershipId: id,
|
||||||
|
email: ""
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { memberships };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:projectId/memberships",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().describe("The ID of the project.")
|
||||||
|
}),
|
||||||
|
|
||||||
|
body: z.object({
|
||||||
|
emails: z.string().email().array().describe("Emails of the users to remove from the project.")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
memberships: ProjectMembershipsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const memberships = await server.services.projectMembership.deleteProjectMemberships({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
emails: req.body.emails
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const membership of memberships) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.REMOVE_WORKSPACE_MEMBER,
|
||||||
|
metadata: {
|
||||||
|
userId: membership.userId,
|
||||||
|
email: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { memberships };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,11 +1,23 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectKeysSchema } from "@app/db/schemas";
|
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
|
const projectWithEnv = ProjectsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
_id: z.string(),
|
||||||
|
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
|
/* Get project key */
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId/encrypted-key",
|
url: "/:workspaceId/encrypted-key",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -34,8 +46,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
const key = await server.services.projectKey.getLatestProjectKey({
|
const key = await server.services.projectKey.getLatestProjectKey({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
projectId: req.params.workspaceId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorOrgId: req.permission.orgId
|
projectId: req.params.workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@ -52,4 +64,107 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Start upgrade of a project */
|
||||||
|
server.route({
|
||||||
|
url: "/:projectId/upgrade",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
|
||||||
|
body: z.object({
|
||||||
|
userPrivateKey: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.void()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.project.upgradeProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
userPrivateKey: req.body.userPrivateKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Get upgrade status of project */
|
||||||
|
server.route({
|
||||||
|
url: "/:projectId/upgrade/status",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
status: z.string().nullable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const status = await server.services.project.getProjectUpgradeStatus({
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Create new project */
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
projectName: z.string().trim(),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(5)
|
||||||
|
.max(36)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Slug must be a valid slug"
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
project: projectWithEnv
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const project = await server.services.project.createProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
workspaceName: req.body.projectName,
|
||||||
|
slug: req.body.slug
|
||||||
|
});
|
||||||
|
|
||||||
|
server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.ProjectCreated,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
name: project.name,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { project };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { FastifyRequest } from "fastify";
|
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -13,6 +12,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
@ -20,19 +20,6 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
|||||||
|
|
||||||
import { secretRawSchema } from "../sanitizedSchemas";
|
import { secretRawSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
const getDistinctId = (req: FastifyRequest) => {
|
|
||||||
if (req.auth.actor === ActorType.USER) {
|
|
||||||
return req.auth.user.email;
|
|
||||||
}
|
|
||||||
if (req.auth.actor === ActorType.IDENTITY) {
|
|
||||||
return `identity-${req.auth.identityId}`;
|
|
||||||
}
|
|
||||||
if (req.auth.actor === ActorType.SERVICE) {
|
|
||||||
return `service-token-${req.auth.serviceToken.id}`;
|
|
||||||
}
|
|
||||||
return "unknown-auth-data";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
url: "/raw",
|
url: "/raw",
|
||||||
@ -110,7 +97,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretPulled,
|
event: PostHogEventTypes.SecretPulled,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -200,7 +187,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretPulled,
|
event: PostHogEventTypes.SecretPulled,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -276,7 +263,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretCreated,
|
event: PostHogEventTypes.SecretCreated,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -351,7 +338,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretUpdated,
|
event: PostHogEventTypes.SecretUpdated,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -421,7 +408,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretDeleted,
|
event: PostHogEventTypes.SecretDeleted,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -527,7 +514,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
if (shouldCapture) {
|
if (shouldCapture) {
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretPulled,
|
event: PostHogEventTypes.SecretPulled,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: shouldRecordK8Event ? approximateNumberTotalSecrets : secrets.length,
|
numberOfSecrets: shouldRecordK8Event ? approximateNumberTotalSecrets : secrets.length,
|
||||||
workspaceId: req.query.workspaceId,
|
workspaceId: req.query.workspaceId,
|
||||||
@ -604,7 +591,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretPulled,
|
event: PostHogEventTypes.SecretPulled,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId: req.query.workspaceId,
|
workspaceId: req.query.workspaceId,
|
||||||
@ -767,7 +754,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretCreated,
|
event: PostHogEventTypes.SecretCreated,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -949,7 +936,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretUpdated,
|
event: PostHogEventTypes.SecretUpdated,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -1067,7 +1054,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretDeleted,
|
event: PostHogEventTypes.SecretDeleted,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -1187,7 +1174,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretCreated,
|
event: PostHogEventTypes.SecretCreated,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -1307,7 +1294,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretUpdated,
|
event: PostHogEventTypes.SecretUpdated,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
@ -1415,7 +1402,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.services.telemetry.sendPostHogEvents({
|
server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretDeleted,
|
event: PostHogEventTypes.SecretDeleted,
|
||||||
distinctId: getDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
workspaceId: req.body.workspaceId,
|
workspaceId: req.body.workspaceId,
|
||||||
|
@ -2,7 +2,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { UsersSchema } from "@app/db/schemas";
|
import { UsersSchema } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||||
@ -23,8 +25,26 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
await server.services.signup.beginEmailSignupProcess(req.body.email);
|
const { email } = req.body;
|
||||||
return { message: `Sent an email verification code to ${req.body.email}` };
|
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
|
if (!serverCfg.allowSignUp) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Sign up is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverCfg?.allowedSignUpDomain) {
|
||||||
|
const domain = email.split("@")[1];
|
||||||
|
const allowedDomains = serverCfg.allowedSignUpDomain.split(",").map((e) => e.trim());
|
||||||
|
if (!allowedDomains.includes(domain)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Email with a domain (@${domain}) is not supported`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await server.services.signup.beginEmailSignupProcess(email);
|
||||||
|
return { message: `Sent an email verification code to ${email}` };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,6 +68,13 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
|
if (!serverCfg.allowSignUp) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Sign up is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { token, user } = await server.services.signup.verifyEmailSignup(req.body.email, req.body.code);
|
const { token, user } = await server.services.signup.verifyEmailSignup(req.body.email, req.body.code);
|
||||||
return { message: "Successfuly verified email", token, user };
|
return { message: "Successfuly verified email", token, user };
|
||||||
}
|
}
|
||||||
@ -90,6 +117,13 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
if (!userAgent) throw new Error("user agent header is required");
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
|
if (!serverCfg.allowSignUp) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Sign up is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { user, accessToken, refreshToken } = await server.services.signup.completeEmailAccountSignup({
|
const { user, accessToken, refreshToken } = await server.services.signup.completeEmailAccountSignup({
|
||||||
...req.body,
|
...req.body,
|
||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
|
@ -4,6 +4,7 @@ import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
@ -261,21 +262,27 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
|||||||
/*
|
/*
|
||||||
* OAuth2 login for google,github, and other oauth2 provider
|
* OAuth2 login for google,github, and other oauth2 provider
|
||||||
* */
|
* */
|
||||||
const oauth2Login = async ({
|
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
authMethod,
|
|
||||||
callbackPort,
|
|
||||||
isSignupAllowed
|
|
||||||
}: TOauthLoginDTO) => {
|
|
||||||
let user = await userDAL.findUserByEmail(email);
|
let user = await userDAL.findUserByEmail(email);
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const isOauthSignUpDisabled = !isSignupAllowed && !user;
|
|
||||||
if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
|
// Create a new user based on oAuth
|
||||||
|
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
|
||||||
|
|
||||||
|
if (serverCfg?.allowedSignUpDomain) {
|
||||||
|
const domain = email.split("@")[1];
|
||||||
|
const allowedDomains = serverCfg.allowedSignUpDomain.split(",").map((e) => e.trim());
|
||||||
|
if (!allowedDomains.includes(domain))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Email with a domain (@${domain}) is not supported`,
|
||||||
|
name: "Oauth 2 login"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], isGhost: false });
|
||||||
}
|
}
|
||||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||||
const isUserCompleted = user.isAccepted;
|
const isUserCompleted = user.isAccepted;
|
||||||
|
@ -28,5 +28,4 @@ export type TOauthLoginDTO = {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
callbackPort?: string;
|
callbackPort?: string;
|
||||||
isSignupAllowed?: boolean;
|
|
||||||
};
|
};
|
||||||
|
@ -50,7 +50,7 @@ export const authSignupServiceFactory = ({
|
|||||||
throw new Error("Failed to send verification code for complete account");
|
throw new Error("Failed to send verification code for complete account");
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email });
|
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, isGhost: false });
|
||||||
}
|
}
|
||||||
if (!user) throw new Error("Failed to create user");
|
if (!user) throw new Error("Failed to create user");
|
||||||
|
|
||||||
|
@ -17,21 +17,24 @@ export enum AuthTokenType {
|
|||||||
API_KEY = "apiKey",
|
API_KEY = "apiKey",
|
||||||
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
|
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
|
||||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
|
||||||
|
SCIM_TOKEN = "scimToken"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthMode {
|
export enum AuthMode {
|
||||||
JWT = "jwt",
|
JWT = "jwt",
|
||||||
SERVICE_TOKEN = "serviceToken",
|
SERVICE_TOKEN = "serviceToken",
|
||||||
API_KEY = "apiKey",
|
API_KEY = "apiKey",
|
||||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
|
||||||
|
SCIM_TOKEN = "scimToken"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActorType { // would extend to AWS, Azure, ...
|
export enum ActorType { // would extend to AWS, Azure, ...
|
||||||
USER = "user", // userIdentity
|
USER = "user", // userIdentity
|
||||||
SERVICE = "service",
|
SERVICE = "service",
|
||||||
IDENTITY = "identity",
|
IDENTITY = "identity",
|
||||||
Machine = "machine"
|
Machine = "machine",
|
||||||
|
SCIM_CLIENT = "scimClient"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthModeJwtTokenPayload = {
|
export type AuthModeJwtTokenPayload = {
|
||||||
|
@ -1,10 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas";
|
||||||
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TIntegrationAuthDALFactory = ReturnType<typeof integrationAuthDALFactory>;
|
export type TIntegrationAuthDALFactory = ReturnType<typeof integrationAuthDALFactory>;
|
||||||
|
|
||||||
export const integrationAuthDALFactory = (db: TDbClient) => {
|
export const integrationAuthDALFactory = (db: TDbClient) => {
|
||||||
const integrationAuthOrm = ormify(db, TableName.IntegrationAuth);
|
const integrationAuthOrm = ormify(db, TableName.IntegrationAuth);
|
||||||
return integrationAuthOrm;
|
|
||||||
|
const bulkUpdate = async (
|
||||||
|
data: Array<{ filter: Partial<TIntegrationAuths>; data: TIntegrationAuthsUpdate }>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const integrationAuths = await Promise.all(
|
||||||
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
|
const [doc] = await (tx || db)(TableName.IntegrationAuth).where(filter).update(updateData).returning("*");
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||||
|
return doc;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return integrationAuths;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...integrationAuthOrm,
|
||||||
|
bulkUpdate
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -76,7 +76,8 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
);
|
)
|
||||||
|
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
|
||||||
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||||
...data,
|
...data,
|
||||||
user: { email, firstName, lastName, id: userId, publicKey }
|
user: { email, firstName, lastName, id: userId, publicKey }
|
||||||
@ -86,6 +87,79 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOrgMembersByEmail = async (orgId: string, emails: string[]) => {
|
||||||
|
try {
|
||||||
|
const members = await db(TableName.OrgMembership)
|
||||||
|
.where({ orgId })
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin<TUserEncryptionKeys>(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("role").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("status").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
|
)
|
||||||
|
.where({ isGhost: false })
|
||||||
|
.whereIn("email", emails);
|
||||||
|
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||||
|
...data,
|
||||||
|
user: { email, firstName, lastName, id: userId, publicKey }
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find all org members" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOrgGhostUser = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const member = await db(TableName.OrgMembership)
|
||||||
|
.where({ orgId })
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("role").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("status").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
|
)
|
||||||
|
.where({ isGhost: true })
|
||||||
|
.first();
|
||||||
|
return member;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ghostUserExists = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const member = await db(TableName.OrgMembership)
|
||||||
|
.where({ orgId })
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(db.ref("id").withSchema(TableName.Users).as("userId"))
|
||||||
|
.where({ isGhost: true })
|
||||||
|
.first();
|
||||||
|
return Boolean(member);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
|
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
|
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
|
||||||
@ -165,7 +239,14 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||||
.select(selectAllTableCols(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users));
|
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.OrgMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("scimEnabled").withSchema(TableName.Organization)
|
||||||
|
);
|
||||||
if (limit) void query.limit(limit);
|
if (limit) void query.limit(limit);
|
||||||
if (offset) void query.offset(offset);
|
if (offset) void query.offset(offset);
|
||||||
if (sort) {
|
if (sort) {
|
||||||
@ -184,6 +265,9 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
findAllOrgMembers,
|
findAllOrgMembers,
|
||||||
findOrgById,
|
findOrgById,
|
||||||
findAllOrgsByUserId,
|
findAllOrgsByUserId,
|
||||||
|
ghostUserExists,
|
||||||
|
findOrgMembersByEmail,
|
||||||
|
findOrgGhostUser,
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
deleteById,
|
deleteById,
|
||||||
|
41
backend/src/services/org/org-fns.ts
Normal file
41
backend/src/services/org/org-fns.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
|
||||||
|
type TDeleteOrgMembership = {
|
||||||
|
orgMembershipId: string;
|
||||||
|
orgId: string;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteOrgMembership = async ({
|
||||||
|
orgMembershipId,
|
||||||
|
orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
}: TDeleteOrgMembership) => {
|
||||||
|
const membership = await orgDAL.transaction(async (tx) => {
|
||||||
|
// delete org membership
|
||||||
|
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
|
||||||
|
|
||||||
|
const projects = await projectDAL.find({ orgId }, { tx });
|
||||||
|
|
||||||
|
// delete associated project memberships
|
||||||
|
await projectMembershipDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
projectId: projects.map((project) => project.id)
|
||||||
|
},
|
||||||
|
userId: orgMembership.userId as string
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return orgMembership;
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
};
|
@ -1,6 +1,8 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import crypto from "crypto";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||||
import { TProjects } from "@app/db/schemas/projects";
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
@ -11,6 +13,7 @@ import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
@ -28,6 +31,7 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
|
|||||||
import {
|
import {
|
||||||
TDeleteOrgMembershipDTO,
|
TDeleteOrgMembershipDTO,
|
||||||
TFindAllWorkspacesDTO,
|
TFindAllWorkspacesDTO,
|
||||||
|
TFindOrgMembersByEmailDTO,
|
||||||
TInviteUserToOrgDTO,
|
TInviteUserToOrgDTO,
|
||||||
TUpdateOrgDTO,
|
TUpdateOrgDTO,
|
||||||
TUpdateOrgMembershipDTO,
|
TUpdateOrgMembershipDTO,
|
||||||
@ -93,6 +97,15 @@ export const orgServiceFactory = ({
|
|||||||
return members;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const members = await orgDAL.findOrgMembersByEmail(orgId, emails);
|
||||||
|
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => {
|
const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
@ -118,6 +131,54 @@ export const orgServiceFactory = ({
|
|||||||
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addGhostUser = async (orgId: string, tx?: Knex) => {
|
||||||
|
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
|
||||||
|
const password = crypto.randomBytes(128).toString("hex");
|
||||||
|
|
||||||
|
const user = await userDAL.create(
|
||||||
|
{
|
||||||
|
isGhost: true,
|
||||||
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
email,
|
||||||
|
isAccepted: true
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const encKeys = await generateUserSrpKeys(email, password);
|
||||||
|
|
||||||
|
await userDAL.upsertUserEncryptionKey(
|
||||||
|
user.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
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const createMembershipData = {
|
||||||
|
orgId,
|
||||||
|
userId: user.id,
|
||||||
|
role: OrgMembershipRole.Admin,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
};
|
||||||
|
|
||||||
|
await orgDAL.createMembership(createMembershipData, tx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
keys: encKeys
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Update organization details
|
* Update organization details
|
||||||
* */
|
* */
|
||||||
@ -126,16 +187,32 @@ export const orgServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
orgId,
|
orgId,
|
||||||
data: { name, slug, authEnforced }
|
data: { name, slug, authEnforced, scimEnabled }
|
||||||
}: TUpdateOrgDTO) => {
|
}: TUpdateOrgDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
if (authEnforced !== undefined) {
|
if (authEnforced !== undefined) {
|
||||||
|
if (!plan?.samlSSO)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO."
|
||||||
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authEnforced) {
|
if (scimEnabled !== undefined) {
|
||||||
|
if (!plan?.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning."
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authEnforced || scimEnabled) {
|
||||||
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
||||||
if (!samlCfg)
|
if (!samlCfg)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@ -147,7 +224,8 @@ export const orgServiceFactory = ({
|
|||||||
const org = await orgDAL.updateById(orgId, {
|
const org = await orgDAL.updateById(orgId, {
|
||||||
name,
|
name,
|
||||||
slug: slug ? slugify(slug) : undefined,
|
slug: slug ? slugify(slug) : undefined,
|
||||||
authEnforced
|
authEnforced,
|
||||||
|
scimEnabled
|
||||||
});
|
});
|
||||||
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
||||||
return org;
|
return org;
|
||||||
@ -321,7 +399,8 @@ export const orgServiceFactory = ({
|
|||||||
{
|
{
|
||||||
email: inviteeEmail,
|
email: inviteeEmail,
|
||||||
isAccepted: false,
|
isAccepted: false,
|
||||||
authMethods: [AuthMethod.EMAIL]
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
isGhost: false
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@ -470,10 +549,12 @@ export const orgServiceFactory = ({
|
|||||||
inviteUserToOrganization,
|
inviteUserToOrganization,
|
||||||
verifyUserToOrg,
|
verifyUserToOrg,
|
||||||
updateOrg,
|
updateOrg,
|
||||||
|
findOrgMembersByEmail,
|
||||||
createOrganization,
|
createOrganization,
|
||||||
deleteOrganizationById,
|
deleteOrganizationById,
|
||||||
deleteOrgMembership,
|
deleteOrgMembership,
|
||||||
findAllWorkspaces,
|
findAllWorkspaces,
|
||||||
|
addGhostUser,
|
||||||
updateOrgMembership,
|
updateOrgMembership,
|
||||||
// incident contacts
|
// incident contacts
|
||||||
findIncidentContacts,
|
findIncidentContacts,
|
||||||
|
@ -30,6 +30,13 @@ export type TVerifyUserToOrgDTO = {
|
|||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TFindOrgMembersByEmailDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
orgId: string;
|
||||||
|
emails: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type TFindAllWorkspacesDTO = {
|
export type TFindAllWorkspacesDTO = {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@ -38,5 +45,5 @@ export type TFindAllWorkspacesDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateOrgDTO = {
|
export type TUpdateOrgDTO = {
|
||||||
data: Partial<{ name: string; slug: string; authEnforced: boolean }>;
|
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
@ -27,5 +27,19 @@ export const projectBotDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...projectBotOrm, findOne };
|
const findProjectByBotId = async (botId: string) => {
|
||||||
|
try {
|
||||||
|
const project = await db(TableName.ProjectBot)
|
||||||
|
.where({ [`${TableName.ProjectBot}.id` as "id"]: botId })
|
||||||
|
.join(TableName.Project, `${TableName.ProjectBot}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Project))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return project || null;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project by bot id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...projectBotOrm, findOne, findProjectByBotId };
|
||||||
};
|
};
|
||||||
|
@ -1,125 +1,111 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
import {
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
decryptAsymmetric,
|
|
||||||
decryptSymmetric,
|
|
||||||
decryptSymmetric128BitHexKeyUTF8,
|
|
||||||
encryptSymmetric,
|
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
|
||||||
generateAsymmetricKeyPair
|
|
||||||
} from "@app/lib/crypto";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
|
||||||
|
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectBotDALFactory } from "./project-bot-dal";
|
import { TProjectBotDALFactory } from "./project-bot-dal";
|
||||||
import { TSetActiveStateDTO } from "./project-bot-types";
|
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
|
||||||
|
|
||||||
type TProjectBotServiceFactoryDep = {
|
type TProjectBotServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
projectBotDAL: TProjectBotDALFactory;
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
||||||
|
|
||||||
export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => {
|
export const projectBotServiceFactory = ({
|
||||||
const getBotKey = async (projectId: string) => {
|
projectBotDAL,
|
||||||
const appCfg = getConfig();
|
projectDAL,
|
||||||
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
permissionService
|
||||||
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
}: TProjectBotServiceFactoryDep) => {
|
||||||
|
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
|
||||||
|
infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const getBotKey = async (projectId: string) => {
|
||||||
const bot = await projectBotDAL.findOne({ projectId });
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
||||||
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
||||||
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
||||||
throw new BadRequestError({ message: "Encryption key missing" });
|
throw new BadRequestError({ message: "Encryption key missing" });
|
||||||
|
|
||||||
if (rootEncryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.BASE64) {
|
const botPrivateKey = getBotPrivateKey({ bot });
|
||||||
const privateKeyBot = decryptSymmetric({
|
|
||||||
iv: bot.iv,
|
|
||||||
tag: bot.tag,
|
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
|
||||||
key: rootEncryptionKey
|
|
||||||
});
|
|
||||||
return decryptAsymmetric({
|
|
||||||
ciphertext: bot.encryptedProjectKey,
|
|
||||||
privateKey: privateKeyBot,
|
|
||||||
nonce: bot.encryptedProjectKeyNonce,
|
|
||||||
publicKey: bot.sender.publicKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (encryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.UTF8) {
|
|
||||||
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
iv: bot.iv,
|
|
||||||
tag: bot.tag,
|
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
|
||||||
key: encryptionKey
|
|
||||||
});
|
|
||||||
return decryptAsymmetric({
|
|
||||||
ciphertext: bot.encryptedProjectKey,
|
|
||||||
privateKey: privateKeyBot,
|
|
||||||
nonce: bot.encryptedProjectKeyNonce,
|
|
||||||
publicKey: bot.sender.publicKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestError({
|
return decryptAsymmetric({
|
||||||
message: "Failed to obtain bot copy of workspace key needed for operation"
|
ciphertext: bot.encryptedProjectKey,
|
||||||
|
privateKey: botPrivateKey,
|
||||||
|
nonce: bot.encryptedProjectKeyNonce,
|
||||||
|
publicKey: bot.sender.publicKey
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const findBotByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TProjectPermission) => {
|
const findBotByProjectId = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
projectId,
|
||||||
|
actorOrgId,
|
||||||
|
privateKey,
|
||||||
|
botKey,
|
||||||
|
publicKey
|
||||||
|
}: TFindBotByProjectIdDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const bot = await projectBotDAL.transaction(async (tx) => {
|
const bot = await projectBotDAL.transaction(async (tx) => {
|
||||||
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
||||||
if (doc) return doc;
|
if (doc) return doc;
|
||||||
|
|
||||||
const { publicKey, privateKey } = generateAsymmetricKeyPair();
|
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
|
||||||
if (appCfg.ROOT_ENCRYPTION_KEY) {
|
|
||||||
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(keys.privateKey);
|
||||||
return projectBotDAL.create(
|
|
||||||
{
|
const project = await projectDAL.findById(projectId, tx);
|
||||||
name: "Infisical Bot",
|
|
||||||
projectId,
|
if (project.version === ProjectVersion.V2) {
|
||||||
tag,
|
throw new BadRequestError({ message: "Failed to create bot, project is upgraded." });
|
||||||
iv,
|
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
isActive: false,
|
|
||||||
publicKey,
|
|
||||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
|
||||||
keyEncoding: SecretKeyEncoding.BASE64
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (appCfg.ENCRYPTION_KEY) {
|
|
||||||
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(privateKey, appCfg.ENCRYPTION_KEY);
|
return projectBotDAL.create(
|
||||||
return projectBotDAL.create(
|
{
|
||||||
{
|
name: "Infisical Bot",
|
||||||
name: "Infisical Bot",
|
projectId,
|
||||||
projectId,
|
tag,
|
||||||
tag,
|
iv,
|
||||||
iv,
|
encryptedPrivateKey: ciphertext,
|
||||||
encryptedPrivateKey: ciphertext,
|
isActive: false,
|
||||||
isActive: false,
|
publicKey: keys.publicKey,
|
||||||
publicKey,
|
algorithm,
|
||||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
keyEncoding: encoding,
|
||||||
keyEncoding: SecretKeyEncoding.UTF8
|
...(botKey && {
|
||||||
},
|
encryptedProjectKey: botKey.encryptedKey,
|
||||||
tx
|
encryptedProjectKeyNonce: botKey.nonce
|
||||||
);
|
})
|
||||||
}
|
},
|
||||||
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
|
tx
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return bot;
|
return bot;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProjectByBotId = async (botId: string) => {
|
||||||
|
try {
|
||||||
|
const bot = await projectBotDAL.findProjectByBotId(botId);
|
||||||
|
return bot;
|
||||||
|
} catch (e) {
|
||||||
|
throw new BadRequestError({ message: "Failed to find bot by ID" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => {
|
const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => {
|
||||||
const bot = await projectBotDAL.findById(botId);
|
const bot = await projectBotDAL.findById(botId);
|
||||||
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
||||||
@ -127,6 +113,16 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
|||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, bot.projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, bot.projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
|
const project = await projectBotDAL.findProjectByBotId(botId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new BadRequestError({ message: "Failed to find project by bot ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.version === ProjectVersion.V2) {
|
||||||
|
throw new BadRequestError({ message: "Failed to set bot active for upgraded project. Bot is already active" });
|
||||||
|
}
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
if (!botKey?.nonce || !botKey?.encryptedKey) {
|
if (!botKey?.nonce || !botKey?.encryptedKey) {
|
||||||
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
|
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
|
||||||
@ -153,6 +149,8 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
|||||||
return {
|
return {
|
||||||
findBotByProjectId,
|
findBotByProjectId,
|
||||||
setBotActiveState,
|
setBotActiveState,
|
||||||
|
getBotPrivateKey,
|
||||||
|
findProjectByBotId,
|
||||||
getBotKey
|
getBotKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TProjectBots } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TSetActiveStateDTO = {
|
export type TSetActiveStateDTO = {
|
||||||
@ -8,3 +9,16 @@ export type TSetActiveStateDTO = {
|
|||||||
};
|
};
|
||||||
botId: string;
|
botId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TFindBotByProjectIdDTO = {
|
||||||
|
privateKey?: string;
|
||||||
|
publicKey?: string;
|
||||||
|
botKey?: {
|
||||||
|
nonce: string;
|
||||||
|
encryptedKey: string;
|
||||||
|
};
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetPrivateKeyDTO = {
|
||||||
|
bot: TProjectBots;
|
||||||
|
};
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TProjectKeys } from "@app/db/schemas";
|
import { TableName, TProjectKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
@ -10,10 +12,11 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findLatestProjectKey = async (
|
const findLatestProjectKey = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string
|
projectId: string,
|
||||||
|
tx?: Knex
|
||||||
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
||||||
try {
|
try {
|
||||||
const projectKey = await db(TableName.ProjectKeys)
|
const projectKey = await (tx || db)(TableName.ProjectKeys)
|
||||||
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||||
.where({ projectId, receiverId: userId })
|
.where({ projectId, receiverId: userId })
|
||||||
@ -29,9 +32,9 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAllProjectUserPubKeys = async (projectId: string) => {
|
const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const pubKeys = await db(TableName.ProjectMembership)
|
const pubKeys = await (tx || db)(TableName.ProjectMembership)
|
||||||
.where({ projectId })
|
.where({ projectId })
|
||||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
||||||
|
|
||||||
@ -24,20 +24,63 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||||
db.ref("role").withSchema(TableName.ProjectMembership),
|
db.ref("role").withSchema(TableName.ProjectMembership),
|
||||||
db.ref("roleId").withSchema(TableName.ProjectMembership),
|
db.ref("roleId").withSchema(TableName.ProjectMembership),
|
||||||
|
db.ref("isGhost").withSchema(TableName.Users),
|
||||||
db.ref("email").withSchema(TableName.Users),
|
db.ref("email").withSchema(TableName.Users),
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||||
db.ref("firstName").withSchema(TableName.Users),
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId")
|
db.ref("id").withSchema(TableName.Users).as("userId")
|
||||||
);
|
)
|
||||||
return members.map(({ email, firstName, lastName, publicKey, ...data }) => ({
|
.where({ isGhost: false });
|
||||||
|
return members.map(({ email, firstName, lastName, publicKey, isGhost, ...data }) => ({
|
||||||
...data,
|
...data,
|
||||||
user: { email, firstName, lastName, id: data.userId, publicKey }
|
user: { email, firstName, lastName, id: data.userId, publicKey, isGhost }
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find all project members" });
|
throw new DatabaseError({ error, name: "Find all project members" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...projectMemberOrm, findAllProjectMembers };
|
const findProjectGhostUser = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const ghostUser = await db(TableName.ProjectMembership)
|
||||||
|
.where({ projectId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Users))
|
||||||
|
.where({ isGhost: true })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return ghostUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project top-level user" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findMembershipsByEmail = async (projectId: string, emails: string[]) => {
|
||||||
|
try {
|
||||||
|
const members = await db(TableName.ProjectMembership)
|
||||||
|
.where({ projectId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.join<TUserEncryptionKeys>(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.ProjectMembership),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("email").withSchema(TableName.Users)
|
||||||
|
)
|
||||||
|
.whereIn("email", emails)
|
||||||
|
.where({ isGhost: false });
|
||||||
|
return members.map(({ userId, email, ...data }) => ({
|
||||||
|
...data,
|
||||||
|
user: { id: userId, email }
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find members by email" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail };
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,28 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
import {
|
||||||
|
OrgMembershipStatus,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectVersion,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
TableName,
|
||||||
|
TProjectMemberships,
|
||||||
|
TUsers
|
||||||
|
} from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
@ -17,7 +30,9 @@ import { TUserDALFactory } from "../user/user-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||||
import {
|
import {
|
||||||
TAddUsersToWorkspaceDTO,
|
TAddUsersToWorkspaceDTO,
|
||||||
TDeleteProjectMembershipDTO,
|
TAddUsersToWorkspaceNonE2EEDTO,
|
||||||
|
TDeleteProjectMembershipOldDTO,
|
||||||
|
TDeleteProjectMembershipsDTO,
|
||||||
TGetProjectMembershipDTO,
|
TGetProjectMembershipDTO,
|
||||||
TInviteUserToProjectDTO,
|
TInviteUserToProjectDTO,
|
||||||
TUpdateProjectMembershipDTO
|
TUpdateProjectMembershipDTO
|
||||||
@ -26,11 +41,12 @@ import {
|
|||||||
type TProjectMembershipServiceFactoryDep = {
|
type TProjectMembershipServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
@ -42,6 +58,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@ -55,64 +72,90 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return projectMembershipDAL.findAllProjectMembers(projectId);
|
return projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, email }: TInviteUserToProjectDTO) => {
|
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, emails }: TInviteUserToProjectDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
const invitee = await userDAL.findOne({ email });
|
const invitees: TUsers[] = [];
|
||||||
if (!invitee || !invitee.isAccepted)
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Faield to validate invitee",
|
|
||||||
name: "Invite user to project"
|
|
||||||
});
|
|
||||||
|
|
||||||
const inviteeMembership = await projectMembershipDAL.findOne({
|
|
||||||
userId: invitee.id,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
if (inviteeMembership)
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Existing member of project",
|
|
||||||
name: "Invite user to project"
|
|
||||||
});
|
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
const inviteeMembershipOrg = await orgDAL.findMembership({
|
const users = await userDAL.find({
|
||||||
userId: invitee.id,
|
$in: { email: emails }
|
||||||
orgId: project.orgId,
|
|
||||||
status: OrgMembershipStatus.Accepted
|
|
||||||
});
|
});
|
||||||
if (!inviteeMembershipOrg)
|
|
||||||
throw new BadRequestError({
|
await projectDAL.transaction(async (tx) => {
|
||||||
message: "Failed to validate invitee org membership",
|
for (const invitee of users) {
|
||||||
name: "Invite user to project"
|
if (!invitee.isAccepted)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate invitee",
|
||||||
|
name: "Invite user to project"
|
||||||
|
});
|
||||||
|
|
||||||
|
const inviteeMembership = await projectMembershipDAL.findOne(
|
||||||
|
{
|
||||||
|
userId: invitee.id,
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inviteeMembership) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Existing member of project",
|
||||||
|
name: "Invite user to project"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteeMembershipOrg = await orgDAL.findMembership({
|
||||||
|
userId: invitee.id,
|
||||||
|
orgId: project.orgId,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inviteeMembershipOrg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate invitee org membership",
|
||||||
|
name: "Invite user to project"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
userId: invitee.id,
|
||||||
|
projectId,
|
||||||
|
role: ProjectMembershipRole.Member
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
invitees.push(invitee);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
|
subjectLine: "Infisical workspace invitation",
|
||||||
|
recipients: invitees.map((i) => i.email),
|
||||||
|
substitutions: {
|
||||||
|
workspaceName: project.name,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
||||||
await projectMembershipDAL.create({
|
|
||||||
userId: invitee.id,
|
|
||||||
projectId,
|
|
||||||
role: ProjectMembershipRole.Member
|
|
||||||
});
|
|
||||||
|
|
||||||
const sender = await userDAL.findById(actorId);
|
return { invitees, latestKey };
|
||||||
const appCfg = getConfig();
|
|
||||||
await smtpService.sendMail({
|
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
|
||||||
subjectLine: "Infisical workspace invitation",
|
|
||||||
recipients: [invitee.email],
|
|
||||||
substitutions: {
|
|
||||||
inviterFirstName: sender.firstName,
|
|
||||||
inviterEmail: sender.email,
|
|
||||||
workspaceName: project.name,
|
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { invitee, latestKey };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUsersToProject = async ({ projectId, actorId, actor, actorOrgId, members }: TAddUsersToWorkspaceDTO) => {
|
const addUsersToProject = async ({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
members,
|
||||||
|
sendEmails = true
|
||||||
|
}: TAddUsersToWorkspaceDTO) => {
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
@ -134,11 +177,16 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
await projectMembershipDAL.insertMany(
|
await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ userId }) => ({
|
orgMembers.map(({ userId, id: membershipId }) => {
|
||||||
projectId,
|
const role =
|
||||||
userId: userId as string,
|
members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member;
|
||||||
role: ProjectMembershipRole.Member
|
|
||||||
})),
|
return {
|
||||||
|
projectId,
|
||||||
|
userId: userId as string,
|
||||||
|
role
|
||||||
|
};
|
||||||
|
}),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
||||||
@ -153,22 +201,132 @@ export const projectMembershipServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const sender = await userDAL.findById(actorId);
|
|
||||||
const appCfg = getConfig();
|
if (sendEmails) {
|
||||||
await smtpService.sendMail({
|
const appCfg = getConfig();
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
await smtpService.sendMail({
|
||||||
subjectLine: "Infisical workspace invitation",
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
subjectLine: "Infisical workspace invitation",
|
||||||
substitutions: {
|
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
||||||
inviterFirstName: sender.firstName,
|
substitutions: {
|
||||||
inviterEmail: sender.email,
|
workspaceName: project.name,
|
||||||
workspaceName: project.name,
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
callback_url: `${appCfg.SITE_URL}/login`
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
return orgMembers;
|
return orgMembers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addUsersToProjectNonE2EE = async ({
|
||||||
|
projectId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
emails,
|
||||||
|
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);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails);
|
||||||
|
|
||||||
|
if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" });
|
||||||
|
|
||||||
|
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: ProjectMembershipRole.Member,
|
||||||
|
userPublicKey: membership.user.publicKey
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const members: TProjectMemberships[] = [];
|
||||||
|
|
||||||
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
|
const result = await projectMembershipDAL.insertMany(
|
||||||
|
orgMembers.map(({ user }) => ({
|
||||||
|
projectId,
|
||||||
|
userId: user.id,
|
||||||
|
role: ProjectMembershipRole.Member
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
members.push(...result);
|
||||||
|
|
||||||
|
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||||
|
await projectKeyDAL.insertMany(
|
||||||
|
orgMembers.map(({ user, id }) => ({
|
||||||
|
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||||
|
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sendEmails) {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
|
subjectLine: "Infisical workspace invitation",
|
||||||
|
recipients: orgMembers.map(({ user }) => user.email).filter(Boolean),
|
||||||
|
substitutions: {
|
||||||
|
workspaceName: project.name,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/login`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
const updateProjectMembership = async ({
|
const updateProjectMembership = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@ -180,6 +338,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||||
|
|
||||||
|
if (membershipUser?.isGhost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unauthorized member update",
|
||||||
|
name: "Update project membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||||
if (isCustomRole) {
|
if (isCustomRole) {
|
||||||
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
@ -205,16 +372,26 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return membership;
|
return membership;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now.
|
||||||
const deleteProjectMembership = async ({
|
const deleteProjectMembership = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
projectId,
|
projectId,
|
||||||
membershipId
|
membershipId
|
||||||
}: TDeleteProjectMembershipDTO) => {
|
}: TDeleteProjectMembershipOldDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const member = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||||
|
|
||||||
|
if (member?.isGhost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unauthorized member delete",
|
||||||
|
name: "Delete project membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const membership = await projectMembershipDAL.transaction(async (tx) => {
|
const membership = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
|
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
|
||||||
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
|
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
|
||||||
@ -223,11 +400,74 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return membership;
|
return membership;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteProjectMemberships = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
projectId,
|
||||||
|
emails
|
||||||
|
}: TDeleteProjectMembershipsDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project not found",
|
||||||
|
name: "Delete project membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectMembers = await projectMembershipDAL.findMembershipsByEmail(projectId, emails);
|
||||||
|
|
||||||
|
if (projectMembers.length !== emails.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Some users are not part of project",
|
||||||
|
name: "Delete project membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor === ActorType.USER && projectMembers.some(({ user }) => user.id === actorId)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot remove yourself from project",
|
||||||
|
name: "Delete project membership"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberships = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
|
const deletedMemberships = await projectMembershipDAL.delete(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
$in: {
|
||||||
|
id: projectMembers.map(({ id }) => id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectKeyDAL.delete(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
$in: {
|
||||||
|
receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return deletedMemberships;
|
||||||
|
});
|
||||||
|
return memberships;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
inviteUserToProject,
|
inviteUserToProject,
|
||||||
updateProjectMembership,
|
updateProjectMembership,
|
||||||
deleteProjectMembership,
|
addUsersToProjectNonE2EE,
|
||||||
|
deleteProjectMemberships,
|
||||||
|
deleteProjectMembership, // TODO: Remove this
|
||||||
addUsersToProject
|
addUsersToProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||||
|
|
||||||
export type TInviteUserToProjectDTO = {
|
export type TInviteUserToProjectDTO = {
|
||||||
email: string;
|
emails: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectMembershipDTO = {
|
export type TUpdateProjectMembershipDTO = {
|
||||||
@ -11,14 +12,25 @@ export type TUpdateProjectMembershipDTO = {
|
|||||||
role: string;
|
role: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteProjectMembershipDTO = {
|
export type TDeleteProjectMembershipOldDTO = {
|
||||||
membershipId: string;
|
membershipId: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TDeleteProjectMembershipsDTO = {
|
||||||
|
emails: string[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TAddUsersToWorkspaceDTO = {
|
export type TAddUsersToWorkspaceDTO = {
|
||||||
|
sendEmails?: boolean;
|
||||||
members: {
|
members: {
|
||||||
orgMembershipId: string;
|
orgMembershipId: string;
|
||||||
workspaceEncryptedKey: string;
|
workspaceEncryptedKey: string;
|
||||||
workspaceEncryptedNonce: string;
|
workspaceEncryptedNonce: string;
|
||||||
|
projectRole: ProjectMembershipRole;
|
||||||
}[];
|
}[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TAddUsersToWorkspaceNonE2EEDTO = {
|
||||||
|
sendEmails?: boolean;
|
||||||
|
emails: string[];
|
||||||
|
} & TProjectPermission;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { ProjectsSchema, TableName } from "@app/db/schemas";
|
import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
||||||
@ -52,6 +54,32 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProjectGhostUser = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const ghostUser = await db(TableName.ProjectMembership)
|
||||||
|
.where({ projectId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Users))
|
||||||
|
.where({ isGhost: true })
|
||||||
|
.first();
|
||||||
|
return ghostUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find project top-level user" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setProjectUpgradeStatus = async (projectId: string, status: ProjectUpgradeStatus | null, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const data: TProjectsUpdate = {
|
||||||
|
upgradeStatus: status
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await (tx || db)(TableName.Project).where({ id: projectId }).update(data);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Set project upgrade status" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findAllProjectsByIdentity = async (identityId: string) => {
|
const findAllProjectsByIdentity = async (identityId: string) => {
|
||||||
try {
|
try {
|
||||||
const workspaces = await db(TableName.IdentityProjectMembership)
|
const workspaces = await db(TableName.IdentityProjectMembership)
|
||||||
@ -132,10 +160,25 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkProjectUpgradeStatus = async (projectId: string) => {
|
||||||
|
const project = await projectOrm.findById(projectId);
|
||||||
|
const upgradeInProgress =
|
||||||
|
project.upgradeStatus === ProjectUpgradeStatus.InProgress && project.version === ProjectVersion.V1;
|
||||||
|
|
||||||
|
if (upgradeInProgress) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project is currently being upgraded, and secrets cannot be written. Please try again"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...projectOrm,
|
...projectOrm,
|
||||||
findAllProjects,
|
findAllProjects,
|
||||||
|
setProjectUpgradeStatus,
|
||||||
findAllProjectsByIdentity,
|
findAllProjectsByIdentity,
|
||||||
findProjectById
|
findProjectGhostUser,
|
||||||
|
findProjectById,
|
||||||
|
checkProjectUpgradeStatus
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
51
backend/src/services/project/project-fns.ts
Normal file
51
backend/src/services/project/project-fns.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
|
|
||||||
|
import { AddUserToWsDTO } from "./project-types";
|
||||||
|
|
||||||
|
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
|
||||||
|
const plaintextProjectKey = decryptAsymmetric({
|
||||||
|
ciphertext: decryptKey.encryptedKey,
|
||||||
|
nonce: decryptKey.nonce,
|
||||||
|
publicKey: decryptKey.sender.publicKey,
|
||||||
|
privateKey: userPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
|
||||||
|
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
|
||||||
|
plaintextProjectKey,
|
||||||
|
userPublicKey,
|
||||||
|
userPrivateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
orgMembershipId,
|
||||||
|
projectRole: projectMembershipRole,
|
||||||
|
workspaceEncryptedKey: inviteeCipherText,
|
||||||
|
workspaceEncryptedNonce: inviteeNonce
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return newWsMembers;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TCreateProjectKeyDTO = {
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
plainProjectKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCreateProjectKeyDTO) => {
|
||||||
|
// 3. Create a random key that we'll use as the project key.
|
||||||
|
const randomBytes = plainProjectKey || crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
// 4. Encrypt the project key with the users key pair.
|
||||||
|
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
|
||||||
|
randomBytes,
|
||||||
|
publicKey,
|
||||||
|
privateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||||
|
};
|
549
backend/src/services/project/project-queue.ts
Normal file
549
backend/src/services/project/project-queue.ts
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import {
|
||||||
|
IntegrationAuthsSchema,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ProjectUpgradeStatus,
|
||||||
|
ProjectVersion,
|
||||||
|
SecretApprovalRequestsSecretsSchema,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
SecretsSchema,
|
||||||
|
SecretVersionsSchema,
|
||||||
|
TIntegrationAuths,
|
||||||
|
TSecretApprovalRequestsSecrets,
|
||||||
|
TSecrets,
|
||||||
|
TSecretVersions
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
|
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||||
|
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
|
import {
|
||||||
|
decryptIntegrationAuths,
|
||||||
|
decryptSecretApprovals,
|
||||||
|
decryptSecrets,
|
||||||
|
decryptSecretVersions
|
||||||
|
} from "@app/lib/crypto";
|
||||||
|
import {
|
||||||
|
decryptAsymmetric,
|
||||||
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
|
infisicalSymmetricDecrypt,
|
||||||
|
infisicalSymmetricEncypt
|
||||||
|
} from "@app/lib/crypto/encryption";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
|
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||||
|
|
||||||
|
export type TProjectQueueFactory = ReturnType<typeof projectQueueFactory>;
|
||||||
|
|
||||||
|
type TProjectQueueFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "bulkUpdateNoVersionIncrement" | "delete">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "find">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "find" | "create" | "delete" | "insertMany">;
|
||||||
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "find">;
|
||||||
|
secretApprovalSecretDAL: Pick<TSecretApprovalRequestSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "delete" | "create">;
|
||||||
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
||||||
|
integrationAuthDAL: TIntegrationAuthDALFactory;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||||
|
|
||||||
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "find">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "transaction" | "updateById" | "setProjectUpgradeStatus" | "find">;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectQueueFactory = ({
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
userDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
integrationAuthDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
orgService,
|
||||||
|
projectMembershipDAL
|
||||||
|
}: TProjectQueueFactoryDep) => {
|
||||||
|
const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
|
||||||
|
await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
|
||||||
|
attempts: 1,
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: {
|
||||||
|
count: 5 // keep the most recent jobs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.UpgradeProjectToGhost, async ({ data }) => {
|
||||||
|
try {
|
||||||
|
const [project] = await projectDAL.find({
|
||||||
|
id: data.projectId,
|
||||||
|
version: ProjectVersion.V1
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId);
|
||||||
|
|
||||||
|
if (!project || !oldProjectKey) {
|
||||||
|
throw new Error("Project or project key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) {
|
||||||
|
throw new Error("Project upgrade status is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.InProgress); // Set the status to in progress. This is important to prevent multiple upgrades at the same time.
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
// await new Promise((resolve) => setTimeout(resolve, 50_000));
|
||||||
|
|
||||||
|
const userPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: data.encryptedPrivateKey.keyEncoding,
|
||||||
|
ciphertext: data.encryptedPrivateKey.encryptedKey,
|
||||||
|
iv: data.encryptedPrivateKey.encryptedKeyIv,
|
||||||
|
tag: data.encryptedPrivateKey.encryptedKeyTag
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPlainProjectKey = decryptAsymmetric({
|
||||||
|
ciphertext: oldProjectKey.encryptedKey,
|
||||||
|
nonce: oldProjectKey.nonce,
|
||||||
|
publicKey: oldProjectKey.sender.publicKey,
|
||||||
|
privateKey: userPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectEnvs = await projectEnvDAL.find({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectFolders = await folderDAL.find({
|
||||||
|
$in: {
|
||||||
|
envId: projectEnvs.map((env) => env.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all the secrets within the project (as encrypted)
|
||||||
|
const projectIntegrationAuths = await integrationAuthDAL.find({
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
const secrets: TSecrets[] = [];
|
||||||
|
const secretVersions: TSecretVersions[] = [];
|
||||||
|
const approvalSecrets: TSecretApprovalRequestsSecrets[] = [];
|
||||||
|
const folderSecretVersionIdsToDelete: string[] = [];
|
||||||
|
|
||||||
|
for (const folder of projectFolders) {
|
||||||
|
const folderSecrets = await secretDAL.find({ folderId: folder.id });
|
||||||
|
|
||||||
|
const folderSecretVersions = await secretVersionDAL.find(
|
||||||
|
{
|
||||||
|
folderId: folder.id
|
||||||
|
},
|
||||||
|
// Only get the latest 700 secret versions for each folder.
|
||||||
|
{
|
||||||
|
limit: 1000,
|
||||||
|
sort: [["createdAt", "desc"]]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedSecretVersions = await secretVersionDAL.find(
|
||||||
|
{
|
||||||
|
folderId: folder.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Get all the secret versions that are not the latest 700
|
||||||
|
offset: 1000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
folderSecretVersionIdsToDelete.push(...deletedSecretVersions.map((el) => el.id));
|
||||||
|
|
||||||
|
const approvalRequests = await secretApprovalRequestDAL.find({
|
||||||
|
status: RequestState.Open,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
const secretApprovals = await secretApprovalSecretDAL.find({
|
||||||
|
$in: {
|
||||||
|
requestId: approvalRequests.map((el) => el.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets.push(...folderSecrets);
|
||||||
|
secretVersions.push(...folderSecretVersions);
|
||||||
|
approvalSecrets.push(...secretApprovals);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
|
||||||
|
const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey);
|
||||||
|
const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey);
|
||||||
|
const decryptedIntegrationAuths = decryptIntegrationAuths(projectIntegrationAuths, userPrivateKey, oldProjectKey);
|
||||||
|
|
||||||
|
// Get the existing bot and the existing project keys for the members of the project
|
||||||
|
const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
|
||||||
|
const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
|
||||||
|
|
||||||
|
// TRANSACTION START
|
||||||
|
await projectDAL.transaction(async (tx) => {
|
||||||
|
await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
|
||||||
|
|
||||||
|
// Create a ghost user
|
||||||
|
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
|
||||||
|
|
||||||
|
// Create a project key
|
||||||
|
const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
|
||||||
|
plainProjectKey: decryptedPlainProjectKey,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
privateKey: ghostUser.keys.plainPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a new project key for the GHOST
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
receiverId: ghostUser.user.id,
|
||||||
|
encryptedKey: newEncryptedProjectKey,
|
||||||
|
nonce: newEncryptedProjectKeyIv,
|
||||||
|
senderId: ghostUser.user.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a membership for the ghost user
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
userId: ghostUser.user.id,
|
||||||
|
role: ProjectMembershipRole.Admin
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a bot already exists, delete it
|
||||||
|
if (existingBot) {
|
||||||
|
await projectBotDAL.delete({ id: existingBot.id }, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all the existing project keys
|
||||||
|
await projectKeyDAL.delete(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
$in: {
|
||||||
|
id: existingProjectKeys.map((key) => key.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new Error("User latest key not found (V2 Upgrade)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProjectMembers: {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
senderId: string;
|
||||||
|
receiverId: string;
|
||||||
|
projectId: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const key of existingProjectKeys) {
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
|
||||||
|
|
||||||
|
if (!user || !orgMembership) {
|
||||||
|
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [newMember] = assignWorkspaceKeysToMembers({
|
||||||
|
decryptKey: ghostUserLatestKey,
|
||||||
|
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userPublicKey: user.publicKey,
|
||||||
|
orgMembershipId: orgMembership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
newProjectMembers.push({
|
||||||
|
encryptedKey: newMember.workspaceEncryptedKey,
|
||||||
|
nonce: newMember.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId: project.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create project keys for all the old members
|
||||||
|
await projectKeyDAL.insertMany(newProjectMembers, tx);
|
||||||
|
|
||||||
|
// Encrypt the bot private key (which is the same as the ghost user)
|
||||||
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||||
|
|
||||||
|
// 5. Create a bot for the project
|
||||||
|
const newBot = await projectBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId: project.id,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
encryptedProjectKey: newEncryptedProjectKey,
|
||||||
|
encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: newBot.iv,
|
||||||
|
tag: newBot.tag,
|
||||||
|
ciphertext: newBot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const botKey = decryptAsymmetric({
|
||||||
|
ciphertext: newBot.encryptedProjectKey!,
|
||||||
|
privateKey: botPrivateKey,
|
||||||
|
nonce: newBot.encryptedProjectKeyNonce!,
|
||||||
|
publicKey: ghostUser.keys.publicKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedSecrets: TSecrets[] = [];
|
||||||
|
const updatedSecretVersions: TSecretVersions[] = [];
|
||||||
|
const updatedSecretApprovals: TSecretApprovalRequestsSecrets[] = [];
|
||||||
|
const updatedIntegrationAuths: TIntegrationAuths[] = [];
|
||||||
|
for (const rawSecret of decryptedSecrets) {
|
||||||
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretKey, botKey);
|
||||||
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretValue || "", botKey);
|
||||||
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||||
|
rawSecret.decrypted.secretComment || "",
|
||||||
|
botKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload: TSecrets = {
|
||||||
|
...rawSecret.original,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8,
|
||||||
|
|
||||||
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
|
||||||
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (!SecretsSchema.safeParse(payload).success) {
|
||||||
|
throw new Error(`Invalid secret payload: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedSecrets.push(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rawSecretVersion of decryptedSecretVersions) {
|
||||||
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretVersion.decrypted.secretKey, botKey);
|
||||||
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||||
|
rawSecretVersion.decrypted.secretValue || "",
|
||||||
|
botKey
|
||||||
|
);
|
||||||
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||||
|
rawSecretVersion.decrypted.secretComment || "",
|
||||||
|
botKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload: TSecretVersions = {
|
||||||
|
...rawSecretVersion.original,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8,
|
||||||
|
|
||||||
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
|
||||||
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (!SecretVersionsSchema.safeParse(payload).success) {
|
||||||
|
throw new Error(`Invalid secret version payload: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedSecretVersions.push(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rawSecretApproval of decryptedApprovalSecrets) {
|
||||||
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretApproval.decrypted.secretKey, botKey);
|
||||||
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||||
|
rawSecretApproval.decrypted.secretValue || "",
|
||||||
|
botKey
|
||||||
|
);
|
||||||
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||||
|
rawSecretApproval.decrypted.secretComment || "",
|
||||||
|
botKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload: TSecretApprovalRequestsSecrets = {
|
||||||
|
...rawSecretApproval.original,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8,
|
||||||
|
|
||||||
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
|
||||||
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (!SecretApprovalRequestsSecretsSchema.safeParse(payload).success) {
|
||||||
|
throw new Error(`Invalid secret approval payload: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedSecretApprovals.push(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const integrationAuth of decryptedIntegrationAuths) {
|
||||||
|
const access = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.access, botKey);
|
||||||
|
const accessId = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.accessId, botKey);
|
||||||
|
const refresh = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.refresh, botKey);
|
||||||
|
|
||||||
|
const payload: TIntegrationAuths = {
|
||||||
|
...integrationAuth.original,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8,
|
||||||
|
|
||||||
|
accessCiphertext: access.ciphertext,
|
||||||
|
accessIV: access.iv,
|
||||||
|
accessTag: access.tag,
|
||||||
|
|
||||||
|
accessIdCiphertext: accessId.ciphertext,
|
||||||
|
accessIdIV: accessId.iv,
|
||||||
|
accessIdTag: accessId.tag,
|
||||||
|
|
||||||
|
refreshCiphertext: refresh.ciphertext,
|
||||||
|
refreshIV: refresh.iv,
|
||||||
|
refreshTag: refresh.tag
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (!IntegrationAuthsSchema.safeParse(payload).success) {
|
||||||
|
throw new Error(`Invalid integration auth payload: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedIntegrationAuths.push(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedSecrets.length !== secrets.length) {
|
||||||
|
throw new Error("Failed to update some secrets");
|
||||||
|
}
|
||||||
|
if (updatedSecretVersions.length !== secretVersions.length) {
|
||||||
|
throw new Error("Failed to update some secret versions");
|
||||||
|
}
|
||||||
|
if (updatedSecretApprovals.length !== approvalSecrets.length) {
|
||||||
|
throw new Error("Failed to update some secret approvals");
|
||||||
|
}
|
||||||
|
if (updatedIntegrationAuths.length !== projectIntegrationAuths.length) {
|
||||||
|
throw new Error("Failed to update some integration auths");
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(updatedSecrets, tx);
|
||||||
|
const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(updatedSecretVersions, tx);
|
||||||
|
const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
|
||||||
|
updatedSecretApprovals,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const integrationAuthUpdates = await integrationAuthDAL.bulkUpdate(
|
||||||
|
updatedIntegrationAuths.map((el) => ({
|
||||||
|
filter: { id: el.id },
|
||||||
|
data: {
|
||||||
|
...el,
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete all secret versions that are no longer needed. We only store the latest 100 versions for each secret.
|
||||||
|
await secretVersionDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: folderSecretVersionIdsToDelete
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
secretUpdates.length !== updatedSecrets.length ||
|
||||||
|
secretVersionUpdates.length !== updatedSecretVersions.length ||
|
||||||
|
secretApprovalUpdates.length !== updatedSecretApprovals.length ||
|
||||||
|
integrationAuthUpdates.length !== updatedIntegrationAuths.length
|
||||||
|
) {
|
||||||
|
throw new Error("Parts of the upgrade failed. Some secrets were not updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectDAL.setProjectUpgradeStatus(data.projectId, null, tx);
|
||||||
|
|
||||||
|
// await new Promise((resolve) => setTimeout(resolve, 15_000));
|
||||||
|
// throw new Error("Transaction was successful!");
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const [project] = await projectDAL
|
||||||
|
.find({
|
||||||
|
id: data.projectId,
|
||||||
|
version: ProjectVersion.V1
|
||||||
|
})
|
||||||
|
.catch(() => [null]);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
logger.error("Failed to upgrade project, because no project was found", data);
|
||||||
|
} else {
|
||||||
|
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
|
||||||
|
logger.error(err, "Failed to upgrade project");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
queueService.listen(QueueName.UpgradeProjectToGhost, "failed", (job, err) => {
|
||||||
|
logger.error(err, "Upgrade project failed", job?.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
upgradeProject
|
||||||
|
};
|
||||||
|
};
|
@ -1,22 +1,40 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||||
|
import { TProjectQueueFactory } from "./project-queue";
|
||||||
|
import {
|
||||||
|
TCreateProjectDTO,
|
||||||
|
TDeleteProjectDTO,
|
||||||
|
TGetProjectDTO,
|
||||||
|
TUpdateProjectDTO,
|
||||||
|
TUpgradeProjectDTO
|
||||||
|
} from "./project-types";
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_ENVS = [
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
{ name: "Development", slug: "dev" },
|
{ name: "Development", slug: "dev" },
|
||||||
@ -26,11 +44,18 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
|
projectQueue: TProjectQueueFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
|
userDAL: TUserDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||||
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,8 +63,15 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
|
|
||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectQueue,
|
||||||
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
orgService,
|
||||||
|
identityProjectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@ -48,8 +80,13 @@ export const projectServiceFactory = ({
|
|||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
* */
|
* */
|
||||||
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName }: TCreateProjectDTO) => {
|
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName, slug }: TCreateProjectDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@ -64,20 +101,28 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newProject = projectDAL.transaction(async (tx) => {
|
const results = await projectDAL.transaction(async (tx) => {
|
||||||
|
const ghostUser = await orgService.addGhostUser(orgId, tx);
|
||||||
|
|
||||||
const project = await projectDAL.create(
|
const project = await projectDAL.create(
|
||||||
{ name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) },
|
{
|
||||||
|
name: workspaceName,
|
||||||
|
orgId,
|
||||||
|
slug: slug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||||
|
version: ProjectVersion.V2
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
// set user as admin member for proeject
|
// set ghost user as admin of project
|
||||||
await projectMembershipDAL.create(
|
await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
userId: actorId,
|
userId: ghostUser.user.id,
|
||||||
role: ProjectMembershipRole.Admin,
|
role: ProjectMembershipRole.Admin,
|
||||||
projectId: project.id
|
projectId: project.id
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
// generate the blind index for project
|
// generate the blind index for project
|
||||||
await secretBlindIndexDAL.create(
|
await secretBlindIndexDAL.create(
|
||||||
{
|
{
|
||||||
@ -99,18 +144,165 @@ export const projectServiceFactory = ({
|
|||||||
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
// _id for backward compat
|
|
||||||
return { ...project, environments: envs, _id: project.id };
|
// 3. Create a random key that we'll use as the project key.
|
||||||
|
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
privateKey: ghostUser.keys.plainPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Save the project key for the ghost user.
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
receiverId: ghostUser.user.id,
|
||||||
|
encryptedKey: encryptedProjectKey,
|
||||||
|
nonce: encryptedProjectKeyIv,
|
||||||
|
senderId: ghostUser.user.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||||
|
|
||||||
|
// 5. Create & a bot for the project
|
||||||
|
await projectBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId: project.id,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedProjectKey,
|
||||||
|
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the ghost users latest key
|
||||||
|
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||||
|
|
||||||
|
if (!latestKey) {
|
||||||
|
throw new Error("Latest key not found for user");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the project is being created by a user, add the user to the project as an admin
|
||||||
|
if (actor === ActorType.USER) {
|
||||||
|
// Find public key of user
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(actorId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [projectAdmin] = assignWorkspaceKeysToMembers({
|
||||||
|
decryptKey: latestKey,
|
||||||
|
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userPublicKey: user.publicKey,
|
||||||
|
orgMembershipId: orgMembership.id,
|
||||||
|
projectMembershipRole: ProjectMembershipRole.Admin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a membership for the user
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
userId: user.id,
|
||||||
|
role: projectAdmin.projectRole
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a project key for the user
|
||||||
|
await projectKeyDAL.create(
|
||||||
|
{
|
||||||
|
encryptedKey: projectAdmin.workspaceEncryptedKey,
|
||||||
|
nonce: projectAdmin.workspaceEncryptedNonce,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
receiverId: user.id,
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the project is being created by an identity, add the identity to the project as an admin
|
||||||
|
else if (actor === ActorType.IDENTITY) {
|
||||||
|
// Find identity org membership
|
||||||
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne(
|
||||||
|
{
|
||||||
|
identityId: actorId,
|
||||||
|
orgId: project.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// If identity org membership not found, throw error
|
||||||
|
if (!identityOrgMembership) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to find identity with id ${actorId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the role permission for the identity
|
||||||
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
|
ProjectMembershipRole.Admin,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPrivilege)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
|
||||||
|
await identityProjectDAL.create(
|
||||||
|
{
|
||||||
|
identityId: actorId,
|
||||||
|
projectId: project.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
environments: envs,
|
||||||
|
_id: project.id
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return newProject;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => {
|
const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
const deletedProject = await projectDAL.deleteById(projectId);
|
const deletedProject = await projectDAL.transaction(async (tx) => {
|
||||||
|
const project = await projectDAL.deleteById(projectId, tx);
|
||||||
|
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(projectId).catch(() => null);
|
||||||
|
|
||||||
|
// Delete the org membership for the ghost user if it's found.
|
||||||
|
if (projectGhostUser) {
|
||||||
|
await userDAL.deleteById(projectGhostUser.id, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project;
|
||||||
|
});
|
||||||
|
|
||||||
return deletedProject;
|
return deletedProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,6 +316,17 @@ export const projectServiceFactory = ({
|
|||||||
return projectDAL.findProjectById(projectId);
|
return projectDAL.findProjectById(projectId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateProject = async ({ projectId, actor, actorId, actorOrgId, update }: TUpdateProjectDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
const updatedProject = await projectDAL.updateById(projectId, {
|
||||||
|
name: update.name,
|
||||||
|
autoCapitalization: update.autoCapitalization
|
||||||
|
});
|
||||||
|
return updatedProject;
|
||||||
|
};
|
||||||
|
|
||||||
const toggleAutoCapitalization = async ({
|
const toggleAutoCapitalization = async ({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
@ -146,12 +349,55 @@ export const projectServiceFactory = ({
|
|||||||
return updatedProject;
|
return updatedProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
|
||||||
|
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
|
if (membership?.role !== ProjectMembershipRole.Admin) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "User must be admin"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedPrivateKey = infisicalSymmetricEncypt(userPrivateKey);
|
||||||
|
|
||||||
|
await projectQueue.upgradeProject({
|
||||||
|
projectId,
|
||||||
|
startedByUserId: actorId,
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
encryptedKey: encryptedPrivateKey.ciphertext,
|
||||||
|
encryptedKeyIv: encryptedPrivateKey.iv,
|
||||||
|
encryptedKeyTag: encryptedPrivateKey.tag,
|
||||||
|
keyEncoding: encryptedPrivateKey.encoding
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||||
|
|
||||||
|
const project = await projectDAL.findProjectById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Project with id ${projectId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.upgradeStatus || null;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
getProjects,
|
getProjects,
|
||||||
|
updateProject,
|
||||||
|
getProjectUpgradeStatus,
|
||||||
getAProject,
|
getAProject,
|
||||||
toggleAutoCapitalization,
|
toggleAutoCapitalization,
|
||||||
updateName
|
updateName,
|
||||||
|
upgradeProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
export type TCreateProjectDTO = {
|
export type TCreateProjectDTO = {
|
||||||
@ -6,6 +9,7 @@ export type TCreateProjectDTO = {
|
|||||||
actorOrgId?: string;
|
actorOrgId?: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
|
slug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteProjectDTO = {
|
export type TDeleteProjectDTO = {
|
||||||
@ -21,3 +25,24 @@ export type TGetProjectDTO = {
|
|||||||
actorOrgId?: string;
|
actorOrgId?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUpdateProjectDTO = {
|
||||||
|
update: {
|
||||||
|
name?: string;
|
||||||
|
autoCapitalization?: boolean;
|
||||||
|
};
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpgradeProjectDTO = {
|
||||||
|
userPrivateKey: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type AddUserToWsDTO = {
|
||||||
|
decryptKey: TProjectKeys & { sender: { publicKey: string } };
|
||||||
|
userPrivateKey: string;
|
||||||
|
members: {
|
||||||
|
orgMembershipId: string;
|
||||||
|
projectMembershipRole: ProjectMembershipRole;
|
||||||
|
userPublicKey: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
@ -4,6 +4,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@ -21,6 +22,7 @@ type TSecretImportServiceFactoryDep = {
|
|||||||
secretImportDAL: TSecretImportDALFactory;
|
secretImportDAL: TSecretImportDALFactory;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
secretDAL: Pick<TSecretDALFactory, "find">;
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||||
projectEnvDAL: TProjectEnvDALFactory;
|
projectEnvDAL: TProjectEnvDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
@ -34,6 +36,7 @@ export const secretImportServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
projectDAL,
|
||||||
secretDAL
|
secretDAL
|
||||||
}: TSecretImportServiceFactoryDep) => {
|
}: TSecretImportServiceFactoryDep) => {
|
||||||
const createImport = async ({
|
const createImport = async ({
|
||||||
@ -62,6 +65,8 @@ export const secretImportServiceFactory = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
||||||
|
|
||||||
|
@ -22,7 +22,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
// the idea is to use postgres specific function
|
// the idea is to use postgres specific function
|
||||||
// insert with id this will cause a conflict then merge the data
|
// insert with id this will cause a conflict then merge the data
|
||||||
const bulkUpdate = async (data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>, tx?: Knex) => {
|
const bulkUpdate = async (
|
||||||
|
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
|
||||||
|
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const secs = await Promise.all(
|
const secs = await Promise.all(
|
||||||
data.map(async ({ filter, data: updateData }) => {
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
@ -41,6 +45,35 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkUpdateNoVersionIncrement = async (data: TSecrets[], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const existingSecrets = await secretOrm.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: data.map((el) => el.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingSecrets.length !== data.length) {
|
||||||
|
throw new BadRequestError({ message: "Some of the secrets do not exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) return [];
|
||||||
|
|
||||||
|
const updatedSecrets = await (tx || db)(TableName.Secret)
|
||||||
|
.insert(data)
|
||||||
|
.onConflict("id") // this will cause a conflict then merge the data
|
||||||
|
.merge() // Merge the data with the existing data
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
return updatedSecrets;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteMany = async (
|
const deleteMany = async (
|
||||||
data: Array<{ blindIndex: string; type: SecretType }>,
|
data: Array<{ blindIndex: string; type: SecretType }>,
|
||||||
folderId: string,
|
folderId: string,
|
||||||
@ -145,5 +178,13 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
|
return {
|
||||||
|
...secretOrm,
|
||||||
|
update,
|
||||||
|
bulkUpdate,
|
||||||
|
deleteMany,
|
||||||
|
bulkUpdateNoVersionIncrement,
|
||||||
|
findByFolderId,
|
||||||
|
findByBlindIndexes
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import { groupBy, pick } from "@app/lib/fn";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@ -49,6 +50,7 @@ type TSecretServiceFactoryDep = {
|
|||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
secretVersionDAL: TSecretVersionDALFactory;
|
secretVersionDAL: TSecretVersionDALFactory;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
@ -68,6 +70,7 @@ export const secretServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
|
projectDAL,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
secretVersionTagDAL
|
secretVersionTagDAL
|
||||||
@ -281,6 +284,8 @@ export const secretServiceFactory = ({
|
|||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
@ -359,6 +364,8 @@ export const secretServiceFactory = ({
|
|||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
@ -459,6 +466,8 @@ export const secretServiceFactory = ({
|
|||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
@ -650,6 +659,8 @@ export const secretServiceFactory = ({
|
|||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
@ -705,6 +716,8 @@ export const secretServiceFactory = ({
|
|||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
@ -776,6 +789,8 @@ export const secretServiceFactory = ({
|
|||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TSecretVersions } from "@app/db/schemas";
|
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
|
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
|
||||||
@ -36,6 +36,57 @@ export const secretVersionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bulkUpdate = async (
|
||||||
|
data: Array<{ filter: Partial<TSecretVersions>; data: TSecretVersionsUpdate }>,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const secs = await Promise.all(
|
||||||
|
data.map(async ({ filter, data: updateData }) => {
|
||||||
|
const [doc] = await (tx || db)(TableName.SecretVersion)
|
||||||
|
.where(filter)
|
||||||
|
.update(updateData)
|
||||||
|
.increment("version", 1) // TODO: Is this really needed?
|
||||||
|
.returning("*");
|
||||||
|
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||||
|
return doc;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return secs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkUpdateNoVersionIncrement = async (data: TSecretVersions[], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const existingSecretVersions = await secretVersionOrm.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: data.map((el) => el.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingSecretVersions.length !== data.length) {
|
||||||
|
throw new BadRequestError({ message: "Some of the secret versions do not exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) return [];
|
||||||
|
|
||||||
|
const updatedSecretVersions = await (tx || db)(TableName.SecretVersion)
|
||||||
|
.insert(data)
|
||||||
|
.onConflict("id") // this will cause a conflict then merge the data
|
||||||
|
.merge() // Merge the data with the existing data
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
return updatedSecretVersions;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
|
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(TableName.SecretVersion)
|
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(TableName.SecretVersion)
|
||||||
@ -59,5 +110,11 @@ export const secretVersionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId };
|
return {
|
||||||
|
...secretVersionOrm,
|
||||||
|
findLatestVersionMany,
|
||||||
|
bulkUpdate,
|
||||||
|
findLatestVersionByFolderId,
|
||||||
|
bulkUpdateNoVersionIncrement
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,32 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TUsers } from "@app/db/schemas";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TServiceTokenDALFactory = ReturnType<typeof serviceTokenDALFactory>;
|
export type TServiceTokenDALFactory = ReturnType<typeof serviceTokenDALFactory>;
|
||||||
|
|
||||||
export const serviceTokenDALFactory = (db: TDbClient) => {
|
export const serviceTokenDALFactory = (db: TDbClient) => {
|
||||||
const stOrm = ormify(db, TableName.ServiceToken);
|
const stOrm = ormify(db, TableName.ServiceToken);
|
||||||
return stOrm;
|
|
||||||
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db)(TableName.ServiceToken)
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
TableName.Users,
|
||||||
|
`${TableName.Users}.id`,
|
||||||
|
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
|
||||||
|
)
|
||||||
|
.where(`${TableName.ServiceToken}.id`, id)
|
||||||
|
.select(selectAllTableCols(TableName.ServiceToken))
|
||||||
|
.select(db.ref("email").withSchema(TableName.Users).as("createdByEmail"))
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (err) {
|
||||||
|
throw new DatabaseError({ error: err, name: "FindById" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...stOrm, findById };
|
||||||
};
|
};
|
||||||
|
@ -142,7 +142,7 @@ export const serviceTokenServiceFactory = ({
|
|||||||
const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, {
|
const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, {
|
||||||
lastUsed: new Date()
|
lastUsed: new Date()
|
||||||
});
|
});
|
||||||
return updatedToken;
|
return { ...serviceToken, lastUsed: updatedToken.lastUsed };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -25,7 +25,8 @@ export enum SmtpTemplates {
|
|||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
ResetPassword = "passwordReset.handlebars",
|
ResetPassword = "passwordReset.handlebars",
|
||||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||||
WorkspaceInvite = "workspaceInvitation.handlebars"
|
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||||
|
ScimUserProvisioned = "scimUserProvisioned.handlebars"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Organization Invitation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Join your organization on Infisical</h2>
|
||||||
|
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
|
||||||
|
<a href="{{callback_url}}">Join now</a>
|
||||||
|
<h3>What is Infisical?</h3>
|
||||||
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,15 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
<title>Project Invitation</title>
|
<title>Project Invitation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Join your team on Infisical</h2>
|
<h2>Join your team on Infisical</h2>
|
||||||
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
|
<p>You have been invited to a new Infisical project — {{workspaceName}}</p>
|
||||||
<a href="{{callback_url}}">Join now</a>
|
<a href="{{callback_url}}">Join now</a>
|
||||||
<h3>What is Infisical?</h3>
|
<h3>What is Infisical?</h3>
|
||||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
||||||
</body>
|
and configs.</p>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
@ -70,6 +70,7 @@ export const superAdminServiceFactory = ({
|
|||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
superAdmin: true,
|
superAdmin: true,
|
||||||
|
isGhost: false,
|
||||||
isAccepted: true,
|
isAccepted: true,
|
||||||
authMethods: [AuthMethod.EMAIL]
|
authMethods: [AuthMethod.EMAIL]
|
||||||
},
|
},
|
||||||
@ -96,7 +97,11 @@ export const superAdminServiceFactory = ({
|
|||||||
|
|
||||||
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
|
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
|
||||||
|
|
||||||
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, initialOrganizationName);
|
const organization = await orgService.createOrganization(
|
||||||
|
userInfo.user.id,
|
||||||
|
userInfo.user.email,
|
||||||
|
initialOrganizationName
|
||||||
|
);
|
||||||
|
|
||||||
await updateServerCfg({ initialized: true });
|
await updateServerCfg({ initialized: true });
|
||||||
const token = await authService.generateUserTokens({
|
const token = await authService.generateUserTokens({
|
||||||
@ -106,7 +111,7 @@ export const superAdminServiceFactory = ({
|
|||||||
organizationId: undefined
|
organizationId: undefined
|
||||||
});
|
});
|
||||||
// TODO(akhilmhdh-pg): telemetry service
|
// TODO(akhilmhdh-pg): telemetry service
|
||||||
return { token, user: userInfo };
|
return { token, user: userInfo, organization };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -61,8 +61,15 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const flushAll = async () => {
|
||||||
|
if (postHog) {
|
||||||
|
await postHog.shutdownAsync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendLoopsEvent,
|
sendLoopsEvent,
|
||||||
sendPostHogEvents
|
sendPostHogEvents,
|
||||||
|
flushAll
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,11 @@ export enum PostHogEventTypes {
|
|||||||
UserSignedUp = "User Signed Up",
|
UserSignedUp = "User Signed Up",
|
||||||
SecretRotated = "secrets rotated",
|
SecretRotated = "secrets rotated",
|
||||||
SecretScannerFull = "historical cloud secret scan",
|
SecretScannerFull = "historical cloud secret scan",
|
||||||
SecretScannerPush = "cloud secret scan"
|
SecretScannerPush = "cloud secret scan",
|
||||||
|
ProjectCreated = "Project Created",
|
||||||
|
IntegrationCreated = "Integration Created",
|
||||||
|
MachineIdentityCreated = "Machine Identity Created",
|
||||||
|
UserOrgInvitation = "User Org Invitation"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSecretModifiedEvent = {
|
export type TSecretModifiedEvent = {
|
||||||
@ -53,9 +57,57 @@ export type TSecretScannerEvent = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TProjectCreateEvent = {
|
||||||
|
event: PostHogEventTypes.ProjectCreated;
|
||||||
|
properties: {
|
||||||
|
name: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMachineIdentityCreatedEvent = {
|
||||||
|
event: PostHogEventTypes.MachineIdentityCreated;
|
||||||
|
properties: {
|
||||||
|
name: string;
|
||||||
|
orgId: string;
|
||||||
|
identityId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIntegrationCreatedEvent = {
|
||||||
|
event: PostHogEventTypes.IntegrationCreated;
|
||||||
|
properties: {
|
||||||
|
projectId: string;
|
||||||
|
integrationId: string;
|
||||||
|
integration: string; // TODO: fix type
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
url?: string;
|
||||||
|
app?: string;
|
||||||
|
appId?: string;
|
||||||
|
targetEnvironment?: string;
|
||||||
|
targetEnvironmentId?: string;
|
||||||
|
targetService?: string;
|
||||||
|
targetServiceId?: string;
|
||||||
|
path?: string;
|
||||||
|
region?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUserOrgInvitedEvent = {
|
||||||
|
event: PostHogEventTypes.UserOrgInvitation;
|
||||||
|
properties: {
|
||||||
|
inviteeEmail: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TPostHogEvent = { distinctId: string } & (
|
export type TPostHogEvent = { distinctId: string } & (
|
||||||
| TSecretModifiedEvent
|
| TSecretModifiedEvent
|
||||||
| TAdminInitEvent
|
| TAdminInitEvent
|
||||||
| TUserSignedUpEvent
|
| TUserSignedUpEvent
|
||||||
| TSecretScannerEvent
|
| TSecretScannerEvent
|
||||||
|
| TUserOrgInvitedEvent
|
||||||
|
| TMachineIdentityCreatedEvent
|
||||||
|
| TIntegrationCreatedEvent
|
||||||
|
| TProjectCreateEvent
|
||||||
);
|
);
|
||||||
|
@ -23,7 +23,7 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
const findUserEncKeyByEmail = async (email: string) => {
|
const findUserEncKeyByEmail = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
return await db(TableName.Users)
|
return await db(TableName.Users)
|
||||||
.where({ email })
|
.where({ email, isGhost: false })
|
||||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||||
.first();
|
.first();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -47,6 +47,17 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
|
||||||
|
try {
|
||||||
|
return await db(TableName.ProjectMembership)
|
||||||
|
.where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId })
|
||||||
|
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.first();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find user by project membership id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
||||||
@ -111,6 +122,7 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
findUserEncKeyByEmail,
|
findUserEncKeyByEmail,
|
||||||
findUserEncKeyByUserId,
|
findUserEncKeyByUserId,
|
||||||
updateUserEncryptionByUserId,
|
updateUserEncryptionByUserId,
|
||||||
|
findUserByProjectMembershipId,
|
||||||
upsertUserEncryptionKey,
|
upsertUserEncryptionKey,
|
||||||
createUserEncryption,
|
createUserEncryption,
|
||||||
findOneUserAction,
|
findOneUserAction,
|
||||||
|
@ -4,10 +4,12 @@ services:
|
|||||||
db-migration:
|
db-migration:
|
||||||
container_name: infisical-db-migration
|
container_name: infisical-db-migration
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
db:
|
||||||
|
condition: service_healthy
|
||||||
image: infisical/infisical:latest-postgres
|
image: infisical/infisical:latest-postgres
|
||||||
env_file: .env
|
env_file: .env
|
||||||
command: npm run migration:latest
|
command: npm run migration:latest
|
||||||
|
pull_policy: always
|
||||||
networks:
|
networks:
|
||||||
- infisical
|
- infisical
|
||||||
|
|
||||||
@ -16,12 +18,13 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
db-migration:
|
db-migration:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
image: infisical/infisical:latest-postgres
|
image: infisical/infisical:latest-postgres
|
||||||
|
pull_policy: always
|
||||||
env_file: .env
|
env_file: .env
|
||||||
ports:
|
ports:
|
||||||
- 80:8080
|
- 80:8080
|
||||||
@ -52,6 +55,11 @@ services:
|
|||||||
- pg_data:/data/db
|
- pg_data:/data/db
|
||||||
networks:
|
networks:
|
||||||
- infisical
|
- infisical
|
||||||
|
healthcheck:
|
||||||
|
test: "pg_isready --username=${POSTGRES_USER} && psql --username=${POSTGRES_USER} --list"
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create Project"
|
||||||
|
openapi: "POST /api/v2/workspace"
|
||||||
|
---
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: "Delete Project"
|
||||||
|
openapi: "DELETE /api/v1/workspace/{workspaceId}"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
This operation is irreversible. All data associated with the project will be deleted. Please use with caution.
|
||||||
|
</Warning>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user