Compare commits
107 Commits
daniel/err
...
daniel/cre
Author | SHA1 | Date | |
---|---|---|---|
|
787f8318fe | ||
|
9a27873af5 | ||
|
0abab57d83 | ||
|
d5662dfef4 | ||
|
ee2ee48b47 | ||
|
896d977b95 | ||
|
d1966b60a8 | ||
|
e3cbcf5853 | ||
|
bdf1f7c601 | ||
|
24b23d4f90 | ||
|
09c1a5f778 | ||
|
73a9cf01f3 | ||
|
97e860cf21 | ||
|
25f694bbdb | ||
|
fd254fbeec | ||
|
859c556425 | ||
|
a3cad030e5 | ||
|
342e9f99d3 | ||
|
8ed04d0b75 | ||
|
5b5a8ff03f | ||
|
e0199084ad | ||
|
67a6deed72 | ||
|
355113e15d | ||
|
40c589eced | ||
|
ec4f175f73 | ||
|
2273c21eb2 | ||
|
97c2b15e29 | ||
|
2f90ee067b | ||
|
7b64288019 | ||
|
e6e1ed7ca9 | ||
|
73838190fd | ||
|
d32fad87d1 | ||
|
67db9679fa | ||
|
3edd48a8b3 | ||
|
a4091bfcdd | ||
|
24483631a0 | ||
|
0f74a1a011 | ||
|
62d6e3763b | ||
|
39ea7a032f | ||
|
3ac125f9c7 | ||
|
7667a7e665 | ||
|
d7499fc5c5 | ||
|
f6885b239b | ||
|
4928322cdb | ||
|
77e191d63e | ||
|
15c98a1d2e | ||
|
ed757bdeff | ||
|
65241ad8bf | ||
|
6a7760f33f | ||
|
fdc62e21ef | ||
|
32f866f834 | ||
|
fbf52850e8 | ||
|
ab9b207f96 | ||
|
5532b9cfea | ||
|
449d3f0304 | ||
|
f0210c2607 | ||
|
ad88aaf17f | ||
|
0485b56e8d | ||
|
b65842f5c1 | ||
|
22b6e0afcd | ||
|
b0e536e576 | ||
|
54e4314e88 | ||
|
d00b1847cc | ||
|
be02617855 | ||
|
b5065f13c9 | ||
|
659b6d5d19 | ||
|
9c33251c44 | ||
|
1a0896475c | ||
|
7e820745a4 | ||
|
fa63c150dd | ||
|
1a2495a95c | ||
|
d79099946a | ||
|
27afad583b | ||
|
acde0867a0 | ||
|
d44f99bac2 | ||
|
2b35e20b1d | ||
|
da15957c3f | ||
|
208fc3452d | ||
|
ba1db870a4 | ||
|
0741058c1d | ||
|
3a6e79c575 | ||
|
70aa73482e | ||
|
2fa30bdd0e | ||
|
b28fe30bba | ||
|
9ba39e99c6 | ||
|
0e6aed7497 | ||
|
7e11fbe7a3 | ||
|
23abab987f | ||
|
a44b3efeb7 | ||
|
1992a09ac2 | ||
|
efa54e0c46 | ||
|
bde2d5e0a6 | ||
|
4090c894fc | ||
|
221bde01f8 | ||
|
b191a3c2f4 | ||
|
e7f1980b80 | ||
|
cd09f03f0b | ||
|
bc475e0f08 | ||
|
afd6dd5257 | ||
|
3a43d7c5d5 | ||
|
65375886bd | ||
|
8495107849 | ||
|
1fcfab7efa | ||
|
499334eef1 | ||
|
9fd76b8729 | ||
|
80d450e980 | ||
|
f63c6b725b |
49
backend/package-lock.json
generated
@@ -61,10 +61,12 @@
|
|||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
"ldapjs": "^3.0.7",
|
"ldapjs": "^3.0.7",
|
||||||
|
"ldif": "^0.5.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"mongodb": "^6.8.1",
|
"mongodb": "^6.8.1",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
@@ -85,6 +87,7 @@
|
|||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
"sjcl": "^1.0.8",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
@@ -108,6 +111,7 @@
|
|||||||
"@types/jsrp": "^0.2.6",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
|
"@types/mustache": "^4.2.5",
|
||||||
"@types/node": "^20.9.5",
|
"@types/node": "^20.9.5",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
"@types/passport-github": "^1.1.12",
|
||||||
@@ -117,6 +121,7 @@
|
|||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
|
"@types/sjcl": "^1.0.34",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
@@ -7074,6 +7079,13 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mustache": {
|
||||||
|
"version": "4.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz",
|
||||||
|
"integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.9.5",
|
"version": "20.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
||||||
@@ -7296,6 +7308,13 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sjcl": {
|
||||||
|
"version": "1.0.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sjcl/-/sjcl-1.0.34.tgz",
|
||||||
|
"integrity": "sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/uuid": {
|
"node_modules/@types/uuid": {
|
||||||
"version": "9.0.7",
|
"version": "9.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
|
||||||
@@ -13008,6 +13027,12 @@
|
|||||||
"verror": "^1.10.1"
|
"verror": "^1.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ldif": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ldif/-/ldif-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-8s46m/r2lSFO2+DqMxqWiJ10iiL4tuR5LC/KndV+E5//OAOzOx5s3HS5O34PJ5+kyaCA+K2oCaEPaDRfXUnQow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
|
||||||
@@ -13704,6 +13729,15 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mustache": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mustache": "bin/mustache"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mylas": {
|
"node_modules/mylas": {
|
||||||
"version": "2.1.13",
|
"version": "2.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz",
|
||||||
@@ -16397,6 +16431,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sjcl": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==",
|
||||||
|
"license": "(BSD-2-Clause OR GPL-2.0-only)",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
@@ -17874,12 +17917,14 @@
|
|||||||
"node_modules/tweetnacl": {
|
"node_modules/tweetnacl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||||
|
"license": "Unlicense"
|
||||||
},
|
},
|
||||||
"node_modules/tweetnacl-util": {
|
"node_modules/tweetnacl-util": {
|
||||||
"version": "0.15.1",
|
"version": "0.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
||||||
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
|
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==",
|
||||||
|
"license": "Unlicense"
|
||||||
},
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
@@ -71,6 +71,7 @@
|
|||||||
"@types/jsrp": "^0.2.6",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
|
"@types/mustache": "^4.2.5",
|
||||||
"@types/node": "^20.9.5",
|
"@types/node": "^20.9.5",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
"@types/passport-github": "^1.1.12",
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
|
"@types/sjcl": "^1.0.34",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
@@ -158,10 +160,12 @@
|
|||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"knex": "^3.0.1",
|
"knex": "^3.0.1",
|
||||||
"ldapjs": "^3.0.7",
|
"ldapjs": "^3.0.7",
|
||||||
|
"ldif": "^0.5.1",
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"mongodb": "^6.8.1",
|
"mongodb": "^6.8.1",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
@@ -182,6 +186,7 @@
|
|||||||
"safe-regex": "^2.1.1",
|
"safe-regex": "^2.1.1",
|
||||||
"scim-patch": "^0.8.3",
|
"scim-patch": "^0.8.3",
|
||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
|
"sjcl": "^1.0.8",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
@@ -38,6 +38,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
|
|||||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
|
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
@@ -181,6 +182,7 @@ declare module "fastify" {
|
|||||||
orgAdmin: TOrgAdminServiceFactory;
|
orgAdmin: TOrgAdminServiceFactory;
|
||||||
slack: TSlackServiceFactory;
|
slack: TSlackServiceFactory;
|
||||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||||
|
migration: TExternalMigrationServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
8
backend/src/@types/knex.d.ts
vendored
@@ -101,6 +101,9 @@ import {
|
|||||||
TIdentityKubernetesAuths,
|
TIdentityKubernetesAuths,
|
||||||
TIdentityKubernetesAuthsInsert,
|
TIdentityKubernetesAuthsInsert,
|
||||||
TIdentityKubernetesAuthsUpdate,
|
TIdentityKubernetesAuthsUpdate,
|
||||||
|
TIdentityMetadata,
|
||||||
|
TIdentityMetadataInsert,
|
||||||
|
TIdentityMetadataUpdate,
|
||||||
TIdentityOidcAuths,
|
TIdentityOidcAuths,
|
||||||
TIdentityOidcAuthsInsert,
|
TIdentityOidcAuthsInsert,
|
||||||
TIdentityOidcAuthsUpdate,
|
TIdentityOidcAuthsUpdate,
|
||||||
@@ -546,6 +549,11 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityUniversalAuthsInsert,
|
TIdentityUniversalAuthsInsert,
|
||||||
TIdentityUniversalAuthsUpdate
|
TIdentityUniversalAuthsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.IdentityMetadata]: KnexOriginal.CompositeTableType<
|
||||||
|
TIdentityMetadata,
|
||||||
|
TIdentityMetadataInsert,
|
||||||
|
TIdentityMetadataUpdate
|
||||||
|
>;
|
||||||
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
||||||
TIdentityKubernetesAuths,
|
TIdentityKubernetesAuths,
|
||||||
TIdentityKubernetesAuthsInsert,
|
TIdentityKubernetesAuthsInsert,
|
||||||
|
4
backend/src/@types/ldif.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "ldif" {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
|
||||||
|
function parse(input: string, ...args: any[]): any;
|
||||||
|
}
|
@@ -3,34 +3,74 @@ import { Knex } from "knex";
|
|||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||||
|
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||||
// add column approverGroupId to AccessApprovalPolicyApprover
|
|
||||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||||
// make nullable
|
// add column approverGroupId to AccessApprovalPolicyApprover
|
||||||
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
if (!hasAccessApproverGroupId) {
|
||||||
|
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
|
||||||
// make approverUserId nullable
|
// make approverUserId nullable
|
||||||
table.uuid("approverUserId").nullable().alter();
|
if (hasAccessApproverUserId) {
|
||||||
|
table.uuid("approverUserId").nullable().alter();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// add column approverGroupId to SecretApprovalPolicyApprover
|
|
||||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||||
table.uuid("approverGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
// add column approverGroupId to SecretApprovalPolicyApprover
|
||||||
table.uuid("approverUserId").nullable().alter();
|
if (!hasSecretApproverGroupId) {
|
||||||
|
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// make approverUserId nullable
|
||||||
|
if (hasSecretApproverUserId) {
|
||||||
|
table.uuid("approverUserId").nullable().alter();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasAccessApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasAccessApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
|
||||||
|
const hasSecretApproverGroupId = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalPolicyApprover,
|
||||||
|
"approverGroupId"
|
||||||
|
);
|
||||||
|
const hasSecretApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||||
|
|
||||||
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
|
||||||
// remove
|
|
||||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
|
||||||
table.dropColumn("approverGroupId");
|
if (hasAccessApproverGroupId) {
|
||||||
table.uuid("approverUserId").notNullable().alter();
|
table.dropColumn("approverGroupId");
|
||||||
|
}
|
||||||
|
// make approverUserId not nullable
|
||||||
|
if (hasAccessApproverUserId) {
|
||||||
|
table.uuid("approverUserId").notNullable().alter();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove
|
// remove
|
||||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
|
||||||
table.dropColumn("approverGroupId");
|
if (hasSecretApproverGroupId) {
|
||||||
table.uuid("approverUserId").notNullable().alter();
|
table.dropColumn("approverGroupId");
|
||||||
|
}
|
||||||
|
// make approverUserId not nullable
|
||||||
|
if (hasSecretApproverUserId) {
|
||||||
|
table.uuid("approverUserId").notNullable().alter();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityMetadata))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityMetadata, (tb) => {
|
||||||
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
tb.string("key").notNullable();
|
||||||
|
tb.string("value").notNullable();
|
||||||
|
tb.uuid("orgId").notNullable();
|
||||||
|
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
tb.uuid("userId");
|
||||||
|
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
tb.uuid("identityId");
|
||||||
|
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityMetadata);
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.string("iv").nullable().alter();
|
||||||
|
t.string("tag").nullable().alter();
|
||||||
|
t.string("encryptedValue").nullable().alter();
|
||||||
|
|
||||||
|
t.binary("encryptedSecret").nullable();
|
||||||
|
t.string("hashedHex").nullable().alter();
|
||||||
|
|
||||||
|
t.string("identifier", 64).nullable();
|
||||||
|
t.unique("identifier");
|
||||||
|
t.index("identifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.dropColumn("encryptedSecret");
|
||||||
|
|
||||||
|
t.dropColumn("identifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed"))) {
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||||
|
tb.datetime("lastUsed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.OidcConfig, "lastUsed")) {
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||||
|
tb.dropColumn("lastUsed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
backend/src/db/schemas/identity-metadata.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// 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 IdentityMetadataSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid().nullable().optional(),
|
||||||
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityMetadata = z.infer<typeof IdentityMetadataSchema>;
|
||||||
|
export type TIdentityMetadataInsert = Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>;
|
||||||
|
export type TIdentityMetadataUpdate = Partial<Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>>;
|
@@ -31,6 +31,7 @@ export * from "./identity-aws-auths";
|
|||||||
export * from "./identity-azure-auths";
|
export * from "./identity-azure-auths";
|
||||||
export * from "./identity-gcp-auths";
|
export * from "./identity-gcp-auths";
|
||||||
export * from "./identity-kubernetes-auths";
|
export * from "./identity-kubernetes-auths";
|
||||||
|
export * from "./identity-metadata";
|
||||||
export * from "./identity-oidc-auths";
|
export * from "./identity-oidc-auths";
|
||||||
export * from "./identity-org-memberships";
|
export * from "./identity-org-memberships";
|
||||||
export * from "./identity-project-additional-privilege";
|
export * from "./identity-project-additional-privilege";
|
||||||
|
@@ -70,6 +70,8 @@ export enum TableName {
|
|||||||
IdentityProjectMembership = "identity_project_memberships",
|
IdentityProjectMembership = "identity_project_memberships",
|
||||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||||
|
// used by both identity and users
|
||||||
|
IdentityMetadata = "identity_metadata",
|
||||||
ScimToken = "scim_tokens",
|
ScimToken = "scim_tokens",
|
||||||
AccessApprovalPolicy = "access_approval_policies",
|
AccessApprovalPolicy = "access_approval_policies",
|
||||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||||
|
@@ -26,7 +26,8 @@ export const OidcConfigsSchema = z.object({
|
|||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid()
|
orgId: z.string().uuid(),
|
||||||
|
lastUsed: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||||
|
@@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string().nullable().optional(),
|
||||||
iv: z.string(),
|
iv: z.string().nullable().optional(),
|
||||||
tag: z.string(),
|
tag: z.string().nullable().optional(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string().nullable().optional(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
@@ -22,7 +24,9 @@ export const SecretSharingSchema = z.object({
|
|||||||
accessType: z.string().default("anyone"),
|
accessType: z.string().default("anyone"),
|
||||||
name: z.string().nullable().optional(),
|
name: z.string().nullable().optional(),
|
||||||
lastViewedAt: z.date().nullable().optional(),
|
lastViewedAt: z.date().nullable().optional(),
|
||||||
password: z.string().nullable().optional()
|
password: z.string().nullable().optional(),
|
||||||
|
encryptedSecret: zodBuffer.nullable().optional(),
|
||||||
|
identifier: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
@@ -3,10 +3,11 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
|
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -100,6 +100,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
async (req, profile, cb) => {
|
async (req, profile, cb) => {
|
||||||
try {
|
try {
|
||||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||||
|
|
||||||
const email =
|
const email =
|
||||||
profile?.email ??
|
profile?.email ??
|
||||||
// entra sends data in this format
|
// entra sends data in this format
|
||||||
@@ -123,6 +124,14 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userMetadata = Object.keys(profile.attributes || {})
|
||||||
|
.map((key) => {
|
||||||
|
// for the ones like in format: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email
|
||||||
|
const formatedKey = key.startsWith("http") ? key.split("/").at(-1) || "" : key;
|
||||||
|
return { key: formatedKey, value: String((profile.attributes as Record<string, string>)[key]) };
|
||||||
|
})
|
||||||
|
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||||
externalId: profile.nameID,
|
externalId: profile.nameID,
|
||||||
email,
|
email,
|
||||||
@@ -130,7 +139,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
lastName: lastName as string,
|
lastName: lastName as string,
|
||||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string,
|
||||||
|
metadata: userMetadata
|
||||||
});
|
});
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -3,6 +3,7 @@ import { AwsIamProvider } from "./aws-iam";
|
|||||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { ElasticSearchProvider } from "./elastic-search";
|
import { ElasticSearchProvider } from "./elastic-search";
|
||||||
|
import { LdapProvider } from "./ldap";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders } from "./models";
|
||||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||||
import { MongoDBProvider } from "./mongo-db";
|
import { MongoDBProvider } from "./mongo-db";
|
||||||
@@ -20,5 +21,6 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
|
||||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider()
|
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||||
|
[DynamicSecretProviders.Ldap]: LdapProvider()
|
||||||
});
|
});
|
||||||
|
234
backend/src/ee/services/dynamic-secret/providers/ldap.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import ldapjs from "ldapjs";
|
||||||
|
import ldif from "ldif";
|
||||||
|
import mustache from "mustache";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { LdapSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 64)();
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodePassword = (password?: string) => {
|
||||||
|
const quotedPassword = `"${password}"`;
|
||||||
|
const utf16lePassword = Buffer.from(quotedPassword, "utf16le");
|
||||||
|
const base64Password = utf16lePassword.toString("base64");
|
||||||
|
return base64Password;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return alphaNumericNanoId(20);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateLDIF = ({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
ldifTemplate
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password?: string;
|
||||||
|
ldifTemplate: string;
|
||||||
|
}): string => {
|
||||||
|
const data = {
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
EncodedPassword: encodePassword(password)
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderedLdif = mustache.render(ldifTemplate, data);
|
||||||
|
|
||||||
|
return renderedLdif;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LdapProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await LdapSchema.parseAsync(inputs);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = ldapjs.createClient({
|
||||||
|
url: providerInputs.url,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: providerInputs.ca ? providerInputs.ca : null,
|
||||||
|
rejectUnauthorized: !!providerInputs.ca
|
||||||
|
},
|
||||||
|
reconnect: true,
|
||||||
|
bindDN: providerInputs.binddn,
|
||||||
|
bindCredentials: providerInputs.bindpass
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (err: Error) => {
|
||||||
|
client.unbind();
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.bind(providerInputs.binddn, providerInputs.bindpass, (err) => {
|
||||||
|
if (err) {
|
||||||
|
client.unbind();
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
return client.connected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
|
||||||
|
type TEntry = {
|
||||||
|
dn: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
changes: {
|
||||||
|
operation?: string;
|
||||||
|
attribute: {
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
|
value: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
values: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, can be any for ldapjs.Change.modification.values
|
||||||
|
value: any;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let parsedEntries: TEntry[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
parsedEntries = ldif.parse(ldif_file).entries as TEntry[];
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid LDIF format, refer to the documentation at Dynamic secrets > LDAP > LDIF Entries."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnArray: string[] = [];
|
||||||
|
|
||||||
|
for await (const entry of parsedEntries) {
|
||||||
|
const { dn } = entry;
|
||||||
|
let responseDn: string;
|
||||||
|
|
||||||
|
if (entry.type === "add") {
|
||||||
|
const attributes: Record<string, string | string[]> = {};
|
||||||
|
|
||||||
|
entry.changes.forEach((change) => {
|
||||||
|
const attrName = change.attribute.attribute;
|
||||||
|
const attrValue = change.value.value;
|
||||||
|
|
||||||
|
attributes[attrName] = Array.isArray(attrValue) ? attrValue : [attrValue];
|
||||||
|
});
|
||||||
|
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.add(dn, attributes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (entry.type === "modify") {
|
||||||
|
const changes: ldapjs.Change[] = [];
|
||||||
|
|
||||||
|
entry.changes.forEach((change) => {
|
||||||
|
changes.push(
|
||||||
|
new ldapjs.Change({
|
||||||
|
operation: change.operation || "replace",
|
||||||
|
modification: {
|
||||||
|
type: change.attribute.attribute,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
values: change.values.map((value) => value.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.modify(dn, changes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (entry.type === "delete") {
|
||||||
|
responseDn = await new Promise((resolve, reject) => {
|
||||||
|
client.del(dn, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(new BadRequestError({ message: err.message }));
|
||||||
|
} else {
|
||||||
|
resolve(dn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client.unbind();
|
||||||
|
throw new BadRequestError({ message: `Unsupported operation type ${entry.type}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
dnArray.push(responseDn);
|
||||||
|
}
|
||||||
|
client.unbind();
|
||||||
|
return dnArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dnArray = await executeLdif(client, generatedLdif);
|
||||||
|
|
||||||
|
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||||
|
} catch (err) {
|
||||||
|
if (providerInputs.rollbackLdif) {
|
||||||
|
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
||||||
|
await executeLdif(client, rollbackLdif);
|
||||||
|
}
|
||||||
|
throw new BadRequestError({ message: (err as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const connection = await getClient(providerInputs);
|
||||||
|
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
|
||||||
|
|
||||||
|
await executeLdif(connection, revocationLdif);
|
||||||
|
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// Do nothing
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@@ -174,6 +174,17 @@ export const AzureEntraIDSchema = z.object({
|
|||||||
clientSecret: z.string().trim().min(1)
|
clientSecret: z.string().trim().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const LdapSchema = z.object({
|
||||||
|
url: z.string().trim().min(1),
|
||||||
|
binddn: z.string().trim().min(1),
|
||||||
|
bindpass: z.string().trim().min(1),
|
||||||
|
ca: z.string().optional(),
|
||||||
|
|
||||||
|
creationLdif: z.string().min(1),
|
||||||
|
revocationLdif: z.string().min(1),
|
||||||
|
rollbackLdif: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra",
|
Cassandra = "cassandra",
|
||||||
@@ -184,7 +195,8 @@ export enum DynamicSecretProviders {
|
|||||||
ElasticSearch = "elastic-search",
|
ElasticSearch = "elastic-search",
|
||||||
MongoDB = "mongo-db",
|
MongoDB = "mongo-db",
|
||||||
RabbitMq = "rabbit-mq",
|
RabbitMq = "rabbit-mq",
|
||||||
AzureEntraID = "azure-entra-id"
|
AzureEntraID = "azure-entra-id",
|
||||||
|
Ldap = "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
@@ -197,7 +209,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
@@ -34,18 +34,12 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
|||||||
|
|
||||||
// TODO(akhilmhdh): move this to more centralized
|
// TODO(akhilmhdh): move this to more centralized
|
||||||
export const UnpackedPermissionSchema = z.object({
|
export const UnpackedPermissionSchema = z.object({
|
||||||
subject: z.union([z.string().min(1), z.string().array()]).optional(),
|
subject: z
|
||||||
action: z.union([z.string().min(1), z.string().array()]),
|
.union([z.string().min(1), z.string().array()])
|
||||||
conditions: z
|
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
||||||
.object({
|
.optional(),
|
||||||
environment: z.string().optional(),
|
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
||||||
secretPath: z
|
conditions: z.unknown().optional()
|
||||||
.object({
|
|
||||||
$glob: z.string().min(1)
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const unpackPermissions = (permissions: unknown) =>
|
const unpackPermissions = (permissions: unknown) =>
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
||||||
@@ -7,5 +8,22 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
|||||||
export const oidcConfigDALFactory = (db: TDbClient) => {
|
export const oidcConfigDALFactory = (db: TDbClient) => {
|
||||||
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
||||||
|
|
||||||
return { ...oidcCfgOrm };
|
const findEnforceableOidcCfg = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const oidcCfg = await db
|
||||||
|
.replicaNode()(TableName.OidcConfig)
|
||||||
|
.where({
|
||||||
|
orgId,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
.whereNotNull("lastUsed")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return oidcCfg;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find org by id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...oidcCfgOrm, findEnforceableOidcCfg };
|
||||||
};
|
};
|
||||||
|
@@ -314,6 +314,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||||
|
|
||||||
if (user.email && !user.isEmailVerified) {
|
if (user.email && !user.isEmailVerified) {
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||||
@@ -395,7 +397,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenEndpoint,
|
tokenEndpoint,
|
||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
jwksUri,
|
jwksUri,
|
||||||
isActive
|
isActive,
|
||||||
|
lastUsed: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (clientId !== undefined) {
|
if (clientId !== undefined) {
|
||||||
@@ -418,6 +421,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
|
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
|
||||||
|
await orgDAL.updateById(org.id, { authEnforced: false, scimEnabled: false });
|
||||||
return ssoConfig;
|
return ssoConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -168,8 +168,14 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
})
|
})
|
||||||
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||||
// groups specific
|
// groups specific
|
||||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||||
@@ -257,6 +263,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||||
// general
|
// general
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
@@ -267,6 +276,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
key: "projectId",
|
key: "projectId",
|
||||||
parentMapper: ({
|
parentMapper: ({
|
||||||
orgId,
|
orgId,
|
||||||
|
username,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
membershipId,
|
membershipId,
|
||||||
groupMembershipId,
|
groupMembershipId,
|
||||||
@@ -279,6 +289,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
|
username,
|
||||||
id: membershipId || groupMembershipId,
|
id: membershipId || groupMembershipId,
|
||||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||||
@@ -354,6 +365,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
isTemporary: userAdditionalPrivilegesIsTemporary
|
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -399,6 +419,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.IdentityProjectMembership}.id`
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
@@ -415,11 +436,17 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.IdentityProjectMembership}.projectId`,
|
`${TableName.IdentityProjectMembership}.projectId`,
|
||||||
`${TableName.Project}.id`
|
`${TableName.Project}.id`
|
||||||
)
|
)
|
||||||
.where("identityId", identityId)
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.where(`${TableName.IdentityProjectMembership}.identityId`, identityId)
|
||||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
@@ -443,15 +470,19 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db
|
db
|
||||||
.ref("temporaryAccessEndTime")
|
.ref("temporaryAccessEndTime")
|
||||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
.as("identityApTemporaryAccessEndTime")
|
.as("identityApTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
);
|
);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "membershipId",
|
key: "membershipId",
|
||||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
|
||||||
id: membershipId,
|
id: membershipId,
|
||||||
identityId,
|
identityId,
|
||||||
|
username: identityName,
|
||||||
projectId,
|
projectId,
|
||||||
createdAt: membershipCreatedAt,
|
createdAt: membershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt,
|
updatedAt: membershipUpdatedAt,
|
||||||
@@ -489,6 +520,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||||
isTemporary: identityApIsTemporary
|
isTemporary: identityApIsTemporary
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@@ -14,14 +14,19 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
|||||||
].includes(actorAuthMethod);
|
].includes(actorAuthMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {
|
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
|
||||||
if (actorAuthMethod === undefined) {
|
if (actorAuthMethod === undefined) {
|
||||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) {
|
if (
|
||||||
throw new ForbiddenRequestError({ name: "SAML auth enforced, cannot access org-scoped resource" });
|
isOrgSsoEnforced &&
|
||||||
|
actorAuthMethod !== null &&
|
||||||
|
!isAuthMethodSaml(actorAuthMethod) &&
|
||||||
|
actorAuthMethod !== AuthMethod.OIDC
|
||||||
|
) {
|
||||||
|
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isAuthMethodSaml, validateOrgSAML };
|
export { isAuthMethodSaml, validateOrgSSO };
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
export type TBuildProjectPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export type TBuildOrgPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
@@ -1,6 +1,7 @@
|
|||||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
import { MongoQuery } from "@ucast/mongo2js";
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrgMembershipRole,
|
OrgMembershipRole,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { objectify } from "@app/lib/fn";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -19,8 +21,8 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
|
|
||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
import { validateOrgSAML } from "./permission-fns";
|
import { validateOrgSSO } from "./permission-fns";
|
||||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
|
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
||||||
import {
|
import {
|
||||||
buildServiceTokenProjectPermission,
|
buildServiceTokenProjectPermission,
|
||||||
projectAdminPermissions,
|
projectAdminPermissions,
|
||||||
@@ -72,7 +74,7 @@ export const permissionServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||||
const rules = projectUserRoles
|
const rules = projectUserRoles
|
||||||
.map(({ role, permissions }) => {
|
.map(({ role, permissions }) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
@@ -98,9 +100,7 @@ export const permissionServiceFactory = ({
|
|||||||
})
|
})
|
||||||
.reduce((curr, prev) => prev.concat(curr), []);
|
.reduce((curr, prev) => prev.concat(curr), []);
|
||||||
|
|
||||||
return createMongoAbility<ProjectPermissionSet>(rules, {
|
return rules;
|
||||||
conditionsMatcher
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -130,7 +130,7 @@ export const permissionServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
validateOrgSSO(authMethod, membership.orgAuthEnforced);
|
||||||
|
|
||||||
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
||||||
membership?.groups?.map(({ role, customRolePermission }) => ({
|
membership?.groups?.map(({ role, customRolePermission }) => ({
|
||||||
@@ -213,7 +213,7 @@ export const permissionServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced);
|
validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced);
|
||||||
|
|
||||||
// join two permissions and pass to build the final permission set
|
// join two permissions and pass to build the final permission set
|
||||||
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||||
@@ -223,8 +223,32 @@ export const permissionServiceFactory = ({
|
|||||||
permissions
|
permissions
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
||||||
|
const metadataKeyValuePair = objectify(
|
||||||
|
userProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
);
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
username: userProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
permission,
|
||||||
membership: userProjectPermission,
|
membership: userProjectPermission,
|
||||||
hasRole: (role: string) =>
|
hasRole: (role: string) =>
|
||||||
userProjectPermission.roles.findIndex(
|
userProjectPermission.roles.findIndex(
|
||||||
@@ -262,8 +286,32 @@ export const permissionServiceFactory = ({
|
|||||||
permissions
|
permissions
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
||||||
|
const metadataKeyValuePair = objectify(
|
||||||
|
identityProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
);
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
username: identityProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
permission,
|
||||||
membership: identityProjectPermission,
|
membership: identityProjectPermission,
|
||||||
hasRole: (role: string) =>
|
hasRole: (role: string) =>
|
||||||
identityProjectPermission.roles.findIndex(
|
identityProjectPermission.roles.findIndex(
|
||||||
@@ -346,14 +394,22 @@ export const permissionServiceFactory = ({
|
|||||||
if (isCustomRole) {
|
if (isCustomRole) {
|
||||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
||||||
|
const rules = buildProjectPermissionRules([
|
||||||
|
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission([
|
permission: createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
conditionsMatcher
|
||||||
]),
|
}),
|
||||||
role: projectRole
|
role: projectRole
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
|
|
||||||
|
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
return { permission };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -364,6 +420,6 @@ export const permissionServiceFactory = ({
|
|||||||
getOrgPermissionByRole,
|
getOrgPermissionByRole,
|
||||||
getProjectPermissionByRole,
|
getProjectPermissionByRole,
|
||||||
buildOrgPermission,
|
buildOrgPermission,
|
||||||
buildProjectPermission
|
buildProjectPermissionRules
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +1,47 @@
|
|||||||
export type TBuildProjectPermissionDTO = {
|
import picomatch from "picomatch";
|
||||||
permissions?: unknown;
|
import { z } from "zod";
|
||||||
role: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export type TBuildOrgPermissionDTO = {
|
export enum PermissionConditionOperators {
|
||||||
permissions?: unknown;
|
$IN = "$in",
|
||||||
role: string;
|
$ALL = "$all",
|
||||||
}[];
|
$REGEX = "$regex",
|
||||||
|
$EQ = "$eq",
|
||||||
|
$NEQ = "$ne",
|
||||||
|
$GLOB = "$glob"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionConditionSchema = {
|
||||||
|
[PermissionConditionOperators.$IN]: z.string().min(1).array(),
|
||||||
|
[PermissionConditionOperators.$ALL]: z.string().min(1).array(),
|
||||||
|
[PermissionConditionOperators.$REGEX]: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine(
|
||||||
|
(el) => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new RegExp(el);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ message: "Invalid regex pattern" }
|
||||||
|
),
|
||||||
|
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
||||||
|
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
||||||
|
[PermissionConditionOperators.$GLOB]: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine(
|
||||||
|
(el) => {
|
||||||
|
try {
|
||||||
|
picomatch.parse([el]);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ message: "Invalid glob pattern" }
|
||||||
|
)
|
||||||
|
};
|
||||||
|
@@ -1,8 +1,12 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
||||||
|
|
||||||
export enum ProjectPermissionActions {
|
export enum ProjectPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@@ -37,7 +41,25 @@ export enum ProjectPermissionSub {
|
|||||||
Kms = "kms"
|
Kms = "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
// secretName: string;
|
||||||
|
// secretTags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CaslSecretsV2SubjectKnexMapper = (field: string) => {
|
||||||
|
switch (field) {
|
||||||
|
case "secretName":
|
||||||
|
return `${TableName.SecretV2}.key`;
|
||||||
|
case "secretTags":
|
||||||
|
return `${TableName.SecretTag}.slug`;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SecretFolderSubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
@@ -45,11 +67,14 @@ type SubjectFields = {
|
|||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||||
]
|
]
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
(
|
||||||
|
| ProjectPermissionSub.SecretFolders
|
||||||
|
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
||||||
|
)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
@@ -76,128 +101,230 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||||
|
|
||||||
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
|
const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
|
z
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
|
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
|
.transform((el) => (typeof el === "string" ? [el] : el));
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
|
|
||||||
|
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
|
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
|
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
|
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
|
|
||||||
|
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
|
const SecretConditionSchema = z
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
|
.object({
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
|
environment: z.union([
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
]),
|
||||||
|
secretPath: z.union([
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||||
|
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback],
|
export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
|
),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
|
conditions: SecretConditionSchema.describe(
|
||||||
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
|
).optional()
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
|
z.object({
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
|
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
|
)
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
|
}),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
|
}),
|
||||||
|
z.object({
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
|
subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
|
)
|
||||||
|
}),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
|
z.object({
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
|
subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
|
}),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
|
z.object({
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
|
subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
|
)
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
|
z.object({
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
|
subject: z.literal(ProjectPermissionSub.Role).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
|
)
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
|
}),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Integrations).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
}),
|
||||||
|
z.object({
|
||||||
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
|
subject: z.literal(ProjectPermissionSub.Webhooks).describe("The entity this permission pertains to."),
|
||||||
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions.
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
)
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
}),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
|
}),
|
||||||
|
z.object({
|
||||||
// double check if all CRUD are needed for CA and Certificates
|
subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
|
}),
|
||||||
|
z.object({
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
|
subject: z.literal(ProjectPermissionSub.Settings).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
|
)
|
||||||
|
}),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
|
z.object({
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
|
subject: z.literal(ProjectPermissionSub.Environments).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
|
}),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
|
z.object({
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
|
subject: z.literal(ProjectPermissionSub.Tags).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
|
)
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
|
z.object({
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
|
subject: z.literal(ProjectPermissionSub.AuditLogs).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
|
)
|
||||||
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
z.object({
|
||||||
];
|
subject: z.literal(ProjectPermissionSub.IpAllowList).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.CertificateAuthorities).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.PkiCollections).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Project).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Kms).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit]).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
const buildAdminPermissionRules = () => {
|
const buildAdminPermissionRules = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
// Admins get full access to everything
|
// Admins get full access to everything
|
||||||
fullProjectPermissionSet.forEach((permission) => {
|
[
|
||||||
const [action, subject] = permission;
|
ProjectPermissionSub.Secrets,
|
||||||
can(action, subject);
|
ProjectPermissionSub.SecretApproval,
|
||||||
|
ProjectPermissionSub.SecretRotation,
|
||||||
|
ProjectPermissionSub.Member,
|
||||||
|
ProjectPermissionSub.Groups,
|
||||||
|
ProjectPermissionSub.Role,
|
||||||
|
ProjectPermissionSub.Integrations,
|
||||||
|
ProjectPermissionSub.Webhooks,
|
||||||
|
ProjectPermissionSub.Identity,
|
||||||
|
ProjectPermissionSub.ServiceTokens,
|
||||||
|
ProjectPermissionSub.Settings,
|
||||||
|
ProjectPermissionSub.Environments,
|
||||||
|
ProjectPermissionSub.Tags,
|
||||||
|
ProjectPermissionSub.AuditLogs,
|
||||||
|
ProjectPermissionSub.IpAllowList,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities,
|
||||||
|
ProjectPermissionSub.Certificates,
|
||||||
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
|
ProjectPermissionSub.PkiAlerts,
|
||||||
|
ProjectPermissionSub.PkiCollections
|
||||||
|
].forEach((el) => {
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
el as ProjectPermissionSub
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
||||||
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||||
|
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,73 +333,116 @@ export const projectAdminPermissions = buildAdminPermissionRules();
|
|||||||
const buildMemberPermissionRules = () => {
|
const buildMemberPermissionRules = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Webhooks
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.ServiceTokens
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Settings
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Environments
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Tags
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
// double check if all CRUD are needed for CA and Certificates
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
@@ -382,32 +552,19 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
|||||||
|
|
||||||
return set1.size >= set2.size;
|
return set1.size >= set2.size;
|
||||||
};
|
};
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
/*
|
export const SecretV2SubjectFieldMapper = (arg: string) => {
|
||||||
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
|
switch (arg) {
|
||||||
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set.
|
case "environment":
|
||||||
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
|
return null;
|
||||||
*/
|
case "secretPath":
|
||||||
export const validateProjectPermissions = (permissions: unknown) => {
|
return null;
|
||||||
const parsedPermissions =
|
case "secretName":
|
||||||
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
|
return `${TableName.SecretV2}.key`;
|
||||||
|
case "secretTags":
|
||||||
const flattenedPermissions = [...parsedPermissions];
|
return `${TableName.SecretTag}.slug`;
|
||||||
|
default:
|
||||||
for (const perm of flattenedPermissions) {
|
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
|
||||||
const [action, subject] = perm;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!fullProjectPermissionSet.find(
|
|
||||||
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Permission action ${action} on subject ${subject} is not valid`,
|
|
||||||
name: "Create Role"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-enable */
|
|
||||||
|
@@ -23,6 +23,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
|||||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
|
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
@@ -51,6 +52,8 @@ type TSamlConfigServiceFactoryDep = {
|
|||||||
TOrgDALFactory,
|
TOrgDALFactory,
|
||||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
@@ -71,7 +74,8 @@ export const samlConfigServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService
|
smtpService,
|
||||||
|
identityMetadataDAL
|
||||||
}: TSamlConfigServiceFactoryDep) => {
|
}: TSamlConfigServiceFactoryDep) => {
|
||||||
const createSamlCfg = async ({
|
const createSamlCfg = async ({
|
||||||
cert,
|
cert,
|
||||||
@@ -332,7 +336,8 @@ export const samlConfigServiceFactory = ({
|
|||||||
lastName,
|
lastName,
|
||||||
authProvider,
|
authProvider,
|
||||||
orgId,
|
orgId,
|
||||||
relayState
|
relayState,
|
||||||
|
metadata
|
||||||
}: TSamlLoginDTO) => {
|
}: TSamlLoginDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
@@ -386,6 +391,21 @@ export const samlConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata && foundUser.id) {
|
||||||
|
await identityMetadataDAL.delete({ userId: foundUser.id, orgId }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
userId: foundUser.id,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return foundUser;
|
return foundUser;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -474,6 +494,20 @@ export const samlConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata && newUser.id) {
|
||||||
|
await identityMetadataDAL.delete({ userId: newUser.id, orgId }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
userId: newUser?.id,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -53,4 +53,5 @@ export type TSamlLoginDTO = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
// saml thingy
|
// saml thingy
|
||||||
relayState?: string;
|
relayState?: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
@@ -16,6 +16,9 @@ export const KeyStorePrefixes = {
|
|||||||
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
||||||
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
||||||
|
|
||||||
|
WaitUntilReadyProjectEnvironmentOperation: (projectId: string) =>
|
||||||
|
`wait-until-ready-project-environments-operation-${projectId}`,
|
||||||
|
ProjectEnvironmentLock: (projectId: string) => `project-environment-lock-${projectId}` as const,
|
||||||
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
|
@@ -360,7 +360,11 @@ export const ORGANIZATIONS = {
|
|||||||
organizationId: "The ID of the organization to update the membership for.",
|
organizationId: "The ID of the organization to update the membership for.",
|
||||||
membershipId: "The ID of the membership to update.",
|
membershipId: "The ID of the membership to update.",
|
||||||
role: "The new role of the membership.",
|
role: "The new role of the membership.",
|
||||||
isActive: "The active status of the membership"
|
isActive: "The active status of the membership",
|
||||||
|
metadata: {
|
||||||
|
key: "The key for user metadata tag.",
|
||||||
|
value: "The value for user metadata tag."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
DELETE_USER_MEMBERSHIP: {
|
DELETE_USER_MEMBERSHIP: {
|
||||||
organizationId: "The ID of the organization to delete the membership from.",
|
organizationId: "The ID of the organization to delete the membership from.",
|
||||||
|
@@ -23,8 +23,19 @@ export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
|||||||
/**
|
/**
|
||||||
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
||||||
*/
|
*/
|
||||||
const extractPermissions = (ability: MongoAbility) =>
|
const extractPermissions = (ability: MongoAbility) => {
|
||||||
ability.rules.map((permission) => `${permission.action as string}_${permission.subject as string}`);
|
const permissions: string[] = [];
|
||||||
|
ability.rules.forEach((permission) => {
|
||||||
|
if (typeof permission.action === "string") {
|
||||||
|
permissions.push(`${permission.action}_${permission.subject as string}`);
|
||||||
|
} else {
|
||||||
|
permission.action.forEach((permissionAction) => {
|
||||||
|
permissions.push(`${permissionAction}_${permission.subject as string}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return permissions;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
||||||
|
111
backend/src/lib/casl/knex.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { AnyAbility, ExtractSubjectType } from "@casl/ability";
|
||||||
|
import { AbilityQuery, rulesToQuery } from "@casl/ability/extra";
|
||||||
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
|
import { BadRequestError, UnauthorizedError } from "../errors";
|
||||||
|
import { TKnexDynamicOperator } from "../knex/dynamic";
|
||||||
|
|
||||||
|
type TBuildKnexQueryFromCaslDTO<K extends AnyAbility> = {
|
||||||
|
ability: K;
|
||||||
|
subject: ExtractSubjectType<Parameters<K["rulesFor"]>[1]>;
|
||||||
|
action: Parameters<K["rulesFor"]>[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildKnexQueryFromCaslOperators = <K extends AnyAbility>({
|
||||||
|
ability,
|
||||||
|
subject,
|
||||||
|
action
|
||||||
|
}: TBuildKnexQueryFromCaslDTO<K>) => {
|
||||||
|
const query = rulesToQuery(ability, action, subject, (rule) => {
|
||||||
|
if (!rule.ast) throw new Error("Ast not defined");
|
||||||
|
return rule.ast;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query === null) throw new UnauthorizedError({ message: `You don't have permission to do ${action} ${subject}` });
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TFieldMapper<T extends keyof Tables> = {
|
||||||
|
[K in T]: `${K}.${Exclude<keyof Tables[K]["base"], symbol>}`;
|
||||||
|
}[T];
|
||||||
|
|
||||||
|
type TFormatCaslFieldsWithTableNames<T extends keyof Tables> = {
|
||||||
|
// handle if any missing operator else throw error let the app break because this is executing again the db
|
||||||
|
missingOperatorCallback?: (operator: string) => void;
|
||||||
|
fieldMapping: (arg: string) => TFieldMapper<T> | null;
|
||||||
|
dynamicQuery: TKnexDynamicOperator;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatCaslOperatorFieldsWithTableNames = <T extends keyof Tables>({
|
||||||
|
missingOperatorCallback = (arg) => {
|
||||||
|
throw new BadRequestError({ message: `Unknown permission operator: ${arg}` });
|
||||||
|
},
|
||||||
|
dynamicQuery: dynamicQueryAst,
|
||||||
|
fieldMapping
|
||||||
|
}: TFormatCaslFieldsWithTableNames<T>) => {
|
||||||
|
const stack: [TKnexDynamicOperator, TKnexDynamicOperator | null][] = [[dynamicQueryAst, null]];
|
||||||
|
|
||||||
|
while (stack.length) {
|
||||||
|
const [filterAst, parentAst] = stack.pop()!;
|
||||||
|
|
||||||
|
if (filterAst.operator === "and" || filterAst.operator === "or" || filterAst.operator === "not") {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push([el, filterAst]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterAst.operator === "eq" ||
|
||||||
|
filterAst.operator === "ne" ||
|
||||||
|
filterAst.operator === "in" ||
|
||||||
|
filterAst.operator === "endsWith" ||
|
||||||
|
filterAst.operator === "startsWith"
|
||||||
|
) {
|
||||||
|
const attrPath = fieldMapping(filterAst.field);
|
||||||
|
if (attrPath) {
|
||||||
|
filterAst.field = attrPath;
|
||||||
|
} else if (parentAst && Array.isArray(parentAst.value)) {
|
||||||
|
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||||
|
} else throw new Error("Unknown casl field");
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentAst && Array.isArray(parentAst.value)) {
|
||||||
|
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||||
|
} else {
|
||||||
|
missingOperatorCallback?.(filterAst.operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dynamicQueryAst;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertCaslOperatorToKnexOperator = <T extends keyof Tables>(
|
||||||
|
caslKnexOperators: AbilityQuery,
|
||||||
|
fieldMapping: (arg: string) => TFieldMapper<T> | null
|
||||||
|
) => {
|
||||||
|
const value = [];
|
||||||
|
if (caslKnexOperators.$and) {
|
||||||
|
value.push({
|
||||||
|
operator: "not" as const,
|
||||||
|
value: caslKnexOperators.$and as TKnexDynamicOperator[]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (caslKnexOperators.$or) {
|
||||||
|
value.push({
|
||||||
|
operator: "or" as const,
|
||||||
|
value: caslKnexOperators.$or as TKnexDynamicOperator[]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatCaslOperatorFieldsWithTableNames({
|
||||||
|
dynamicQuery: {
|
||||||
|
operator: "and",
|
||||||
|
value
|
||||||
|
},
|
||||||
|
fieldMapping
|
||||||
|
});
|
||||||
|
};
|
@@ -52,3 +52,21 @@ export const unique = <T, K extends string | number | symbol>(array: readonly T[
|
|||||||
);
|
);
|
||||||
return Object.values(valueMap);
|
return Object.values(valueMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array to a dictionary by mapping each item
|
||||||
|
* into a dictionary key & value
|
||||||
|
*/
|
||||||
|
export const objectify = <T, Key extends string | number | symbol, Value = T>(
|
||||||
|
array: readonly T[],
|
||||||
|
getKey: (item: T) => Key,
|
||||||
|
getValue: (item: T) => Value = (item) => item as unknown as Value
|
||||||
|
): Record<Key, Value> => {
|
||||||
|
return array.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
acc[getKey(item)] = getValue(item);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<Key, Value>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
89
backend/src/lib/knex/dynamic.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { UnauthorizedError } from "../errors";
|
||||||
|
|
||||||
|
type TKnexDynamicPrimitiveOperator = {
|
||||||
|
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||||
|
value: string;
|
||||||
|
field: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TKnexDynamicInOperator = {
|
||||||
|
operator: "in";
|
||||||
|
value: string[] | number[];
|
||||||
|
field: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator;
|
||||||
|
|
||||||
|
type TKnexGroupOperator = {
|
||||||
|
operator: "and" | "or" | "not";
|
||||||
|
value: (TKnexNonGroupOperator | TKnexGroupOperator)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// akhilmhdh: This is still in pending state and not yet ready. If you want to use it ping me.
|
||||||
|
// used when you need to write a complex query with the orm
|
||||||
|
// use it when you need complex or and and condition - most of the time not needed
|
||||||
|
// majorly used with casl permission to filter data based on permission
|
||||||
|
export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator;
|
||||||
|
|
||||||
|
export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => {
|
||||||
|
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
|
||||||
|
|
||||||
|
while (stack.length) {
|
||||||
|
const { filterAst, queryBuilder } = stack.pop()!;
|
||||||
|
switch (filterAst.operator) {
|
||||||
|
case "eq": {
|
||||||
|
void queryBuilder.where(filterAst.field, "=", filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ne": {
|
||||||
|
void queryBuilder.whereNot(filterAst.field, filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "startsWith": {
|
||||||
|
void queryBuilder.whereILike(filterAst.field, `${filterAst.value}%`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "endsWith": {
|
||||||
|
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "and": {
|
||||||
|
void queryBuilder.andWhere((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "or": {
|
||||||
|
void queryBuilder.orWhere((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "not": {
|
||||||
|
void queryBuilder.whereNot((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -1,5 +1,5 @@
|
|||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
import { Redis } from "ioredis";
|
// import { Redis } from "ioredis";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -74,7 +74,6 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"
|
|||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
|
||||||
@@ -97,10 +96,12 @@ import { certificateAuthorityServiceFactory } from "@app/services/certificate-au
|
|||||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
|
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
|
import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
|
||||||
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||||
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
||||||
@@ -265,6 +266,7 @@ export const registerRoutes = async (
|
|||||||
const serviceTokenDAL = serviceTokenDALFactory(db);
|
const serviceTokenDAL = serviceTokenDALFactory(db);
|
||||||
|
|
||||||
const identityDAL = identityDALFactory(db);
|
const identityDAL = identityDALFactory(db);
|
||||||
|
const identityMetadataDAL = identityMetadataDALFactory(db);
|
||||||
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
||||||
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
||||||
const identityProjectDAL = identityProjectDALFactory(db);
|
const identityProjectDAL = identityProjectDALFactory(db);
|
||||||
@@ -386,6 +388,7 @@ export const registerRoutes = async (
|
|||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
||||||
|
|
||||||
const samlService = samlConfigServiceFactory({
|
const samlService = samlConfigServiceFactory({
|
||||||
|
identityMetadataDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
@@ -489,6 +492,7 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
const orgService = orgServiceFactory({
|
const orgService = orgServiceFactory({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
|
identityMetadataDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -507,7 +511,8 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL
|
orgBotDAL,
|
||||||
|
oidcConfigDAL
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
@@ -743,6 +748,7 @@ export const registerRoutes = async (
|
|||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
@@ -918,7 +924,8 @@ export const registerRoutes = async (
|
|||||||
const secretSharingService = secretSharingServiceFactory({
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL
|
orgDAL,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||||
@@ -1027,7 +1034,8 @@ export const registerRoutes = async (
|
|||||||
identityDAL,
|
identityDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
identityMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||||
@@ -1186,6 +1194,14 @@ export const registerRoutes = async (
|
|||||||
workflowIntegrationDAL
|
workflowIntegrationDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const migrationService = externalMigrationServiceFactory({
|
||||||
|
projectService,
|
||||||
|
orgService,
|
||||||
|
projectEnvService,
|
||||||
|
permissionService,
|
||||||
|
secretService
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
//
|
//
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@@ -1269,7 +1285,8 @@ export const registerRoutes = async (
|
|||||||
externalKms: externalKmsService,
|
externalKms: externalKmsService,
|
||||||
orgAdmin: orgAdminService,
|
orgAdmin: orgAdminService,
|
||||||
slack: slackService,
|
slack: slackService,
|
||||||
workflowIntegration: workflowIntegrationService
|
workflowIntegration: workflowIntegrationService,
|
||||||
|
migration: migrationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
@@ -1308,33 +1325,33 @@ export const registerRoutes = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async () => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
await db.raw("SELECT NOW()");
|
// await db.raw("SELECT NOW()");
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
logger.error("Health check: database connection failed", err);
|
// logger.error("Health check: database connection failed", err);
|
||||||
return reply.code(503).send({
|
// return reply.code(503).send({
|
||||||
date: new Date(),
|
// date: new Date(),
|
||||||
message: "Service unavailable"
|
// message: "Service unavailable"
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (cfg.isRedisConfigured) {
|
// if (cfg.isRedisConfigured) {
|
||||||
const redis = new Redis(cfg.REDIS_URL);
|
// const redis = new Redis(cfg.REDIS_URL);
|
||||||
try {
|
// try {
|
||||||
await redis.ping();
|
// await redis.ping();
|
||||||
redis.disconnect();
|
// redis.disconnect();
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
logger.error("Health check: redis connection failed", err);
|
// logger.error("Health check: redis connection failed", err);
|
||||||
return reply.code(503).send({
|
// return reply.code(503).send({
|
||||||
date: new Date(),
|
// date: new Date(),
|
||||||
message: "Service unavailable"
|
// message: "Service unavailable"
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
|
@@ -17,6 +17,20 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
|
// handle querystring boolean values
|
||||||
|
const booleanSchema = z
|
||||||
|
.union([z.boolean(), z.string().trim()])
|
||||||
|
.transform((value) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
// ie if not empty, 0 or false, return true
|
||||||
|
return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default(true);
|
||||||
|
|
||||||
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -57,21 +71,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
|
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.orderDirection)
|
||||||
.optional(),
|
.optional(),
|
||||||
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
|
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
|
||||||
includeSecrets: z.coerce
|
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||||
.boolean()
|
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||||
.optional()
|
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||||
.default(true)
|
|
||||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
|
||||||
includeFolders: z.coerce
|
|
||||||
.boolean()
|
|
||||||
.optional()
|
|
||||||
.default(true)
|
|
||||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
|
||||||
includeDynamicSecrets: z.coerce
|
|
||||||
.boolean()
|
|
||||||
.optional()
|
|
||||||
.default(true)
|
|
||||||
.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -198,7 +200,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (includeDynamicSecrets) {
|
if (includeDynamicSecrets && permissiveEnvs.length) {
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -239,7 +241,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeSecrets) {
|
if (includeSecrets && permissiveEnvs.length) {
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -354,26 +356,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional(),
|
.optional(),
|
||||||
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
||||||
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
||||||
includeSecrets: z.coerce
|
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||||
.boolean()
|
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||||
.optional()
|
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||||
.default(true)
|
includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
|
||||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
|
||||||
includeFolders: z.coerce
|
|
||||||
.boolean()
|
|
||||||
.optional()
|
|
||||||
.default(true)
|
|
||||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
|
||||||
includeDynamicSecrets: z.coerce
|
|
||||||
.boolean()
|
|
||||||
.optional()
|
|
||||||
.default(true)
|
|
||||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
|
||||||
includeImports: z.coerce
|
|
||||||
.boolean()
|
|
||||||
.optional()
|
|
||||||
.default(true)
|
|
||||||
.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
@@ -29,7 +29,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().describe(IDENTITIES.CREATE.name),
|
name: z.string().trim().describe(IDENTITIES.CREATE.name),
|
||||||
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
|
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
|
||||||
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role)
|
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role),
|
||||||
|
metadata: z
|
||||||
|
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -93,7 +97,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
|
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
|
||||||
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role)
|
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role),
|
||||||
|
metadata: z
|
||||||
|
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -193,6 +201,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identity: IdentityOrgMembershipsSchema.extend({
|
identity: IdentityOrgMembershipsSchema.extend({
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1),
|
||||||
|
id: z.string().trim().min(1),
|
||||||
|
value: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
customRole: OrgRolesSchema.pick({
|
customRole: OrgRolesSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
||||||
|
|
||||||
import { registerAdminRouter } from "./admin-router";
|
import { registerAdminRouter } from "./admin-router";
|
||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
@@ -101,4 +103,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
||||||
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
|
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
|
||||||
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
||||||
|
|
||||||
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
};
|
};
|
||||||
|
@@ -131,9 +131,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(INTEGRATION.UPDATE.secretPath),
|
.describe(INTEGRATION.UPDATE.secretPath),
|
||||||
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
|
||||||
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
|
||||||
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
|
environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
|
||||||
metadata: IntegrationMetadataSchema.optional()
|
metadata: IntegrationMetadataSchema.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@@ -28,7 +28,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: OrganizationsSchema.extend({
|
||||||
|
orgAuthMethod: z.string()
|
||||||
|
}).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -55,10 +55,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
hashedHex: z.string().min(1),
|
hashedHex: z.string().min(1).optional(),
|
||||||
password: z.string().optional()
|
password: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -73,7 +73,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
accessType: true
|
accessType: true
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
orgName: z.string().optional()
|
orgName: z.string().optional(),
|
||||||
|
secretValue: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
@@ -99,17 +100,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
encryptedValue: z.string(),
|
secretValue: z.string().max(10_000),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
hashedHex: z.string(),
|
|
||||||
iv: z.string(),
|
|
||||||
tag: z.string(),
|
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number().min(1).optional()
|
expiresAfterViews: z.number().min(1).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -132,17 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().max(50).optional(),
|
name: z.string().max(50).optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
encryptedValue: z.string(),
|
secretValue: z.string(),
|
||||||
hashedHex: z.string(),
|
|
||||||
iv: z.string(),
|
|
||||||
tag: z.string(),
|
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number().min(1).optional(),
|
expiresAfterViews: z.number().min(1).optional(),
|
||||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -168,7 +163,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
sharedSecretId: z.string().uuid()
|
sharedSecretId: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SecretSharingSchema
|
200: SecretSharingSchema
|
||||||
|
@@ -130,18 +130,24 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
membership: OrgMembershipsSchema.merge(
|
membership: OrgMembershipsSchema.extend({
|
||||||
z.object({
|
metadata: z
|
||||||
user: UsersSchema.pick({
|
.object({
|
||||||
username: true,
|
key: z.string().trim().min(1),
|
||||||
email: true,
|
id: z.string().trim().min(1),
|
||||||
isEmailVerified: true,
|
value: z.string().trim().min(1)
|
||||||
firstName: true,
|
})
|
||||||
lastName: true,
|
.array()
|
||||||
id: true
|
.optional(),
|
||||||
}).merge(z.object({ publicKey: z.string().nullable() }))
|
user: UsersSchema.pick({
|
||||||
})
|
username: true,
|
||||||
).omit({ createdAt: true, updatedAt: true })
|
email: true,
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
}).extend({ publicKey: z.string().nullable() })
|
||||||
|
}).omit({ createdAt: true, updatedAt: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -178,7 +184,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role),
|
role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role),
|
||||||
isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive)
|
isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1).describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.metadata.key),
|
||||||
|
value: z.string().trim().min(1).describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.metadata.value)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
35
backend/src/server/routes/v3/external-migration-router.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerExternalMigrationRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/env-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
decryptionKey: z.string().trim().min(1),
|
||||||
|
encryptedJson: z.object({
|
||||||
|
nonce: z.string().trim().min(1),
|
||||||
|
data: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.migration.importEnvKeyData({
|
||||||
|
decryptionKey: req.body.decryptionKey,
|
||||||
|
encryptedJson: req.body.encryptedJson,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,4 +1,4 @@
|
|||||||
import { registerDashboardRouter } from "./dashboard-router";
|
import { registerExternalMigrationRouter } from "./external-migration-router";
|
||||||
import { registerLoginRouter } from "./login-router";
|
import { registerLoginRouter } from "./login-router";
|
||||||
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
||||||
import { registerSecretRouter } from "./secret-router";
|
import { registerSecretRouter } from "./secret-router";
|
||||||
@@ -11,5 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
||||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
await server.register(registerExternalMigrationRouter, { prefix: "/migrate" });
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,197 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import sjcl from "sjcl";
|
||||||
|
import tweetnacl from "tweetnacl";
|
||||||
|
import tweetnaclUtil from "tweetnacl-util";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectServiceFactory } from "../project/project-service";
|
||||||
|
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||||
|
import { TSecretServiceFactory } from "../secret/secret-service";
|
||||||
|
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
|
||||||
|
|
||||||
|
export type TImportDataIntoInfisicalDTO = {
|
||||||
|
projectService: TProjectServiceFactory;
|
||||||
|
orgService: TOrgServiceFactory;
|
||||||
|
projectEnvService: TProjectEnvServiceFactory;
|
||||||
|
secretService: TSecretServiceFactory;
|
||||||
|
|
||||||
|
input: TImportInfisicalDataCreate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { codec, hash } = sjcl;
|
||||||
|
const { secretbox } = tweetnacl;
|
||||||
|
|
||||||
|
export const decryptEnvKeyDataFn = async (decryptionKey: string, encryptedJson: { nonce: string; data: string }) => {
|
||||||
|
const key = tweetnaclUtil.decodeBase64(codec.base64.fromBits(hash.sha256.hash(decryptionKey)));
|
||||||
|
const nonce = tweetnaclUtil.decodeBase64(encryptedJson.nonce);
|
||||||
|
const encryptedData = tweetnaclUtil.decodeBase64(encryptedJson.data);
|
||||||
|
|
||||||
|
const decrypted = secretbox.open(encryptedData, nonce, key);
|
||||||
|
|
||||||
|
if (!decrypted) {
|
||||||
|
throw new BadRequestError({ message: "Decryption failed, please check the entered encryption key" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedJson = tweetnaclUtil.encodeUTF8(decrypted);
|
||||||
|
return decryptedJson;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<InfisicalImportData> => {
|
||||||
|
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
|
||||||
|
|
||||||
|
const infisicalImportData: InfisicalImportData = {
|
||||||
|
projects: new Map<string, { name: string; id: string }>(),
|
||||||
|
environments: new Map<string, { name: string; id: string; projectId: string }>(),
|
||||||
|
secrets: new Map<string, { name: string; id: string; projectId: string; environmentId: string; value: string }>()
|
||||||
|
};
|
||||||
|
|
||||||
|
parsedJson.apps.forEach((app: { name: string; id: string }) => {
|
||||||
|
infisicalImportData.projects.set(app.id, { name: app.name, id: app.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
// string to string map for env templates
|
||||||
|
const envTemplates = new Map<string, string>();
|
||||||
|
for (const env of parsedJson.defaultEnvironmentRoles) {
|
||||||
|
envTemplates.set(env.id, env.defaultName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// environments
|
||||||
|
for (const env of parsedJson.baseEnvironments) {
|
||||||
|
infisicalImportData.environments?.set(env.id, {
|
||||||
|
id: env.id,
|
||||||
|
name: envTemplates.get(env.environmentRoleId)!,
|
||||||
|
projectId: env.envParentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// secrets
|
||||||
|
for (const env of Object.keys(parsedJson.envs)) {
|
||||||
|
if (!env.includes("|")) {
|
||||||
|
const envData = parsedJson.envs[env];
|
||||||
|
for (const secret of Object.keys(envData.variables)) {
|
||||||
|
const id = randomUUID();
|
||||||
|
infisicalImportData.secrets?.set(id, {
|
||||||
|
id,
|
||||||
|
name: secret,
|
||||||
|
environmentId: env,
|
||||||
|
value: envData.variables[secret].val
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return infisicalImportData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importDataIntoInfisicalFn = async ({
|
||||||
|
projectService,
|
||||||
|
orgService,
|
||||||
|
projectEnvService,
|
||||||
|
secretService,
|
||||||
|
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
||||||
|
}: TImportDataIntoInfisicalDTO) => {
|
||||||
|
// Import data to infisical
|
||||||
|
if (!data || !data.projects) {
|
||||||
|
throw new BadRequestError({ message: "No projects found in data" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalToNewProjectId = new Map<string, string>();
|
||||||
|
const originalToNewEnvironmentId = new Map<string, string>();
|
||||||
|
|
||||||
|
for await (const [id, project] of data.projects) {
|
||||||
|
const newProject = await projectService
|
||||||
|
.createProject({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
workspaceName: project.name,
|
||||||
|
createDefaultEnvs: false
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
|
||||||
|
});
|
||||||
|
|
||||||
|
originalToNewProjectId.set(project.id, newProject.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite user importing projects
|
||||||
|
const invites = await orgService.inviteUserToOrganization({
|
||||||
|
actorAuthMethod,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actor,
|
||||||
|
inviteeEmails: [],
|
||||||
|
orgId: actorOrgId,
|
||||||
|
organizationRoleSlug: OrgMembershipRole.NoAccess,
|
||||||
|
projects: Array.from(originalToNewProjectId.values()).map((project) => ({
|
||||||
|
id: project,
|
||||||
|
projectRoleSlug: [ProjectMembershipRole.Member]
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
if (!invites) {
|
||||||
|
throw new BadRequestError({ message: `Failed to invite user to projects: [userId:${actorId}]` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import environments
|
||||||
|
if (data.environments) {
|
||||||
|
for await (const [id, environment] of data.environments) {
|
||||||
|
try {
|
||||||
|
const newEnvironment = await projectEnvService.createEnvironment({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
name: environment.name,
|
||||||
|
projectId: originalToNewProjectId.get(environment.projectId)!,
|
||||||
|
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newEnvironment) {
|
||||||
|
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`);
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to import environment: [name:${environment.name}] [id:${id}]`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
originalToNewEnvironmentId.set(id, newEnvironment.slug);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to import environment: ${environment.name}]`,
|
||||||
|
name: "EnvKeyMigrationImportEnvironment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import secrets
|
||||||
|
if (data.secrets) {
|
||||||
|
for await (const [id, secret] of data.secrets) {
|
||||||
|
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId;
|
||||||
|
if (!dataProjectId) {
|
||||||
|
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` });
|
||||||
|
}
|
||||||
|
const projectId = originalToNewProjectId.get(dataProjectId);
|
||||||
|
const newSecret = await secretService.createSecretRaw({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
environment: originalToNewEnvironmentId.get(secret.environmentId)!,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId: projectId!,
|
||||||
|
secretPath: "/",
|
||||||
|
secretName: secret.name,
|
||||||
|
type: SecretType.Shared,
|
||||||
|
secretValue: secret.value
|
||||||
|
});
|
||||||
|
if (!newSecret) {
|
||||||
|
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { OrgMembershipRole } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectServiceFactory } from "../project/project-service";
|
||||||
|
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||||
|
import { TSecretServiceFactory } from "../secret/secret-service";
|
||||||
|
import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
||||||
|
import { TImportEnvKeyDataCreate } from "./external-migration-types";
|
||||||
|
|
||||||
|
type TExternalMigrationServiceFactoryDep = {
|
||||||
|
projectService: TProjectServiceFactory;
|
||||||
|
orgService: TOrgServiceFactory;
|
||||||
|
projectEnvService: TProjectEnvServiceFactory;
|
||||||
|
secretService: TSecretServiceFactory;
|
||||||
|
permissionService: TPermissionServiceFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
|
||||||
|
|
||||||
|
export const externalMigrationServiceFactory = ({
|
||||||
|
projectService,
|
||||||
|
orgService,
|
||||||
|
projectEnvService,
|
||||||
|
permissionService,
|
||||||
|
secretService
|
||||||
|
}: TExternalMigrationServiceFactoryDep) => {
|
||||||
|
const importEnvKeyData = async ({
|
||||||
|
decryptionKey,
|
||||||
|
encryptedJson,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TImportEnvKeyDataCreate) => {
|
||||||
|
const { membership } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (membership.role !== OrgMembershipRole.Admin) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Only admins can import data" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
|
||||||
|
const envKeyData = await parseEnvKeyDataFn(json);
|
||||||
|
const response = await importDataIntoInfisicalFn({
|
||||||
|
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod },
|
||||||
|
projectService,
|
||||||
|
orgService,
|
||||||
|
projectEnvService,
|
||||||
|
secretService
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
importEnvKeyData
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,106 @@
|
|||||||
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export type InfisicalImportData = {
|
||||||
|
projects: Map<string, { name: string; id: string }>;
|
||||||
|
|
||||||
|
environments?: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
secrets?: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
environmentId: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TImportEnvKeyDataCreate = {
|
||||||
|
decryptionKey: string;
|
||||||
|
encryptedJson: { nonce: string; data: string };
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TImportInfisicalDataCreate = {
|
||||||
|
data: InfisicalImportData;
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TEnvKeyExportJSON = {
|
||||||
|
schemaVersion: string;
|
||||||
|
org: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
settings: {
|
||||||
|
auth: {
|
||||||
|
inviteExpirationMs: number;
|
||||||
|
deviceGrantExpirationMs: number;
|
||||||
|
tokenExpirationMs: number;
|
||||||
|
};
|
||||||
|
crypto: {
|
||||||
|
requiresPassphrase: boolean;
|
||||||
|
requiresLockout: boolean;
|
||||||
|
};
|
||||||
|
envs: {
|
||||||
|
autoCaps: boolean;
|
||||||
|
autoCommitLocals: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
apps: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
}[];
|
||||||
|
defaultOrgRoles: {
|
||||||
|
id: string;
|
||||||
|
defaultName: string;
|
||||||
|
}[];
|
||||||
|
defaultAppRoles: {
|
||||||
|
id: string;
|
||||||
|
defaultName: string;
|
||||||
|
}[];
|
||||||
|
defaultEnvironmentRoles: {
|
||||||
|
id: string;
|
||||||
|
defaultName: string;
|
||||||
|
settings: {
|
||||||
|
autoCommit: boolean;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
baseEnvironments: {
|
||||||
|
id: string;
|
||||||
|
envParentId: string;
|
||||||
|
environmentRoleId: string;
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
}[];
|
||||||
|
orgUsers: {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
provider: string;
|
||||||
|
orgRoleId: string;
|
||||||
|
uid: string;
|
||||||
|
}[];
|
||||||
|
envs: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
variables: Record<string, { val: string }>;
|
||||||
|
inherits: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
@@ -1,5 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import axios from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
@@ -107,32 +107,54 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data }: { data: TCreateTokenReviewResponse } = await axios.post(
|
const { data } = await axios
|
||||||
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
|
.post<TCreateTokenReviewResponse>(
|
||||||
{
|
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||||
apiVersion: "authentication.k8s.io/v1",
|
{
|
||||||
kind: "TokenReview",
|
apiVersion: "authentication.k8s.io/v1",
|
||||||
spec: {
|
kind: "TokenReview",
|
||||||
token: serviceAccountJwt
|
spec: {
|
||||||
}
|
token: serviceAccountJwt
|
||||||
},
|
}
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${tokenReviewerJwt}`
|
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
{
|
||||||
ca: caCert,
|
headers: {
|
||||||
rejectUnauthorized: !!caCert
|
"Content-Type": "application/json",
|
||||||
})
|
Authorization: `Bearer ${tokenReviewerJwt}`
|
||||||
}
|
},
|
||||||
);
|
|
||||||
|
|
||||||
if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error });
|
// if ca cert, rejectUnauthorized: true
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
ca: caCert,
|
||||||
|
rejectUnauthorized: !!caCert
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response) {
|
||||||
|
const { message } = err?.response?.data as unknown as { message?: string };
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message,
|
||||||
|
name: "KubernetesTokenReviewRequestError"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("error" in data.status)
|
||||||
|
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
|
||||||
|
|
||||||
// check the response to determine if the token is valid
|
// check the response to determine if the token is valid
|
||||||
if (!(data.status && data.status.authenticated))
|
if (!(data.status && data.status.authenticated))
|
||||||
throw new UnauthorizedError({ message: "Kubernetes token not authenticated" });
|
throw new UnauthorizedError({
|
||||||
|
message: "Kubernetes token not authenticated",
|
||||||
|
name: "KubernetesTokenReviewError"
|
||||||
|
});
|
||||||
|
|
||||||
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
|
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
|
||||||
|
|
||||||
|
10
backend/src/services/identity/identity-metadata-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 TIdentityMetadataDALFactory = ReturnType<typeof identityMetadataDALFactory>;
|
||||||
|
|
||||||
|
export const identityMetadataDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.IdentityMetadata);
|
||||||
|
return orm;
|
||||||
|
};
|
@@ -3,7 +3,7 @@ import { Knex } from "knex";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
@@ -42,10 +42,25 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
const paginatedFetchIdentity = (tx || db.replicaNode())(TableName.Identity)
|
||||||
|
.where((queryBuilder) => {
|
||||||
|
if (limit) {
|
||||||
|
void queryBuilder.offset(offset).limit(limit);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.as(TableName.Identity);
|
||||||
|
|
||||||
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
.join<Awaited<typeof paginatedFetchIdentity>>(paginatedFetchIdentity, (queryBuilder) => {
|
||||||
|
queryBuilder.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`);
|
||||||
|
})
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||||
// cr stands for custom role
|
// cr stands for custom role
|
||||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||||
@@ -55,12 +70,15 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||||
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
||||||
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
.select(
|
||||||
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
|
db.ref("name").as("identityName").withSchema(TableName.Identity),
|
||||||
|
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)
|
||||||
if (limit) {
|
)
|
||||||
void query.offset(offset).limit(limit);
|
.select(
|
||||||
}
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
|
);
|
||||||
|
|
||||||
if (orderBy) {
|
if (orderBy) {
|
||||||
switch (orderBy) {
|
switch (orderBy) {
|
||||||
@@ -80,9 +98,10 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const docs = await query;
|
const docs = await query;
|
||||||
|
const formattedDocs = sqlNestRelationships({
|
||||||
return docs.map(
|
data: docs,
|
||||||
({
|
key: "id",
|
||||||
|
parentMapper: ({
|
||||||
crId,
|
crId,
|
||||||
crDescription,
|
crDescription,
|
||||||
crSlug,
|
crSlug,
|
||||||
@@ -91,16 +110,21 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
identityId,
|
identityId,
|
||||||
identityName,
|
identityName,
|
||||||
identityAuthMethod,
|
identityAuthMethod,
|
||||||
...el
|
role,
|
||||||
|
roleId,
|
||||||
|
id,
|
||||||
|
orgId,
|
||||||
|
createdAt,
|
||||||
|
updatedAt
|
||||||
}) => ({
|
}) => ({
|
||||||
...el,
|
role,
|
||||||
|
roleId,
|
||||||
identityId,
|
identityId,
|
||||||
identity: {
|
id,
|
||||||
id: identityId,
|
orgId,
|
||||||
name: identityName,
|
createdAt,
|
||||||
authMethod: identityAuthMethod
|
updatedAt,
|
||||||
},
|
customRole: roleId
|
||||||
customRole: el.roleId
|
|
||||||
? {
|
? {
|
||||||
id: crId,
|
id: crId,
|
||||||
name: crName,
|
name: crName,
|
||||||
@@ -108,9 +132,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
permissions: crPermission,
|
permissions: crPermission,
|
||||||
description: crDescription
|
description: crDescription
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined,
|
||||||
})
|
identity: {
|
||||||
);
|
id: identityId,
|
||||||
|
name: identityName,
|
||||||
|
authMethod: identityAuthMethod as string
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedDocs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindByOrgId" });
|
throw new DatabaseError({ error, name: "FindByOrgId" });
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
|
|||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "./identity-dal";
|
import { TIdentityDALFactory } from "./identity-dal";
|
||||||
|
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
|
||||||
import { TIdentityOrgDALFactory } from "./identity-org-dal";
|
import { TIdentityOrgDALFactory } from "./identity-org-dal";
|
||||||
import {
|
import {
|
||||||
TCreateIdentityDTO,
|
TCreateIdentityDTO,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
|
|
||||||
type TIdentityServiceFactoryDep = {
|
type TIdentityServiceFactoryDep = {
|
||||||
identityDAL: TIdentityDALFactory;
|
identityDAL: TIdentityDALFactory;
|
||||||
|
identityMetadataDAL: TIdentityMetadataDALFactory;
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
@@ -32,6 +34,7 @@ export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
|||||||
|
|
||||||
export const identityServiceFactory = ({
|
export const identityServiceFactory = ({
|
||||||
identityDAL,
|
identityDAL,
|
||||||
|
identityMetadataDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -44,7 +47,8 @@ export const identityServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
metadata
|
||||||
}: TCreateIdentityDTO) => {
|
}: TCreateIdentityDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||||
@@ -78,6 +82,17 @@ export const identityServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
if (metadata && metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
identityId: newIdentity.id,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
return newIdentity;
|
return newIdentity;
|
||||||
});
|
});
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
@@ -92,7 +107,8 @@ export const identityServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
metadata
|
||||||
}: TUpdateIdentityDTO) => {
|
}: TUpdateIdentityDTO) => {
|
||||||
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
|
||||||
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
|
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
|
||||||
@@ -134,8 +150,8 @@ export const identityServiceFactory = ({
|
|||||||
const identity = await identityDAL.transaction(async (tx) => {
|
const identity = await identityDAL.transaction(async (tx) => {
|
||||||
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
|
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
|
||||||
if (role) {
|
if (role) {
|
||||||
await identityOrgMembershipDAL.update(
|
await identityOrgMembershipDAL.updateById(
|
||||||
{ identityId: id },
|
identityOrgMembership.id,
|
||||||
{
|
{
|
||||||
role: customRole ? OrgMembershipRole.Custom : role,
|
role: customRole ? OrgMembershipRole.Custom : role,
|
||||||
roleId: customRole?.id || null
|
roleId: customRole?.id || null
|
||||||
@@ -143,6 +159,20 @@ export const identityServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (metadata) {
|
||||||
|
await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
identityId: newIdentity.id,
|
||||||
|
orgId: identityOrgMembership.orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return newIdentity;
|
return newIdentity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -4,12 +4,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
|||||||
export type TCreateIdentityDTO = {
|
export type TCreateIdentityDTO = {
|
||||||
role: string;
|
role: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TUpdateIdentityDTO = {
|
export type TUpdateIdentityDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TDeleteIdentityDTO = {
|
export type TDeleteIdentityDTO = {
|
||||||
|
@@ -150,12 +150,17 @@ export const integrationServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
const newEnvironment = environment || integration.environment.slug;
|
||||||
ProjectPermissionActions.Read,
|
const newSecretPath = secretPath || integration.secretPath;
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(integration.projectId, environment, secretPath);
|
if (environment || secretPath) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
|
||||||
if (!folder) throw new NotFoundError({ message: "Folder path not found" });
|
if (!folder) throw new NotFoundError({ message: "Folder path not found" });
|
||||||
|
|
||||||
const updatedIntegration = await integrationDAL.updateById(id, {
|
const updatedIntegration = await integrationDAL.updateById(id, {
|
||||||
@@ -174,7 +179,7 @@ export const integrationServiceFactory = ({
|
|||||||
|
|
||||||
await secretQueueService.syncIntegrations({
|
await secretQueueService.syncIntegrations({
|
||||||
environment: folder.environment.slug,
|
environment: folder.environment.slug,
|
||||||
secretPath,
|
secretPath: newSecretPath,
|
||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,6 +189,12 @@ export const integrationServiceFactory = ({
|
|||||||
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
||||||
const integration = await integrationDAL.findById(id);
|
const integration = await integrationDAL.findById(id);
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Integration not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
|
@@ -48,10 +48,10 @@ export type TUpdateIntegrationDTO = {
|
|||||||
app?: string;
|
app?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
secretPath: string;
|
secretPath?: string;
|
||||||
targetEnvironment: string;
|
targetEnvironment?: string;
|
||||||
owner: string;
|
owner?: string;
|
||||||
environment: string;
|
environment?: string;
|
||||||
metadata?: {
|
metadata?: {
|
||||||
secretPrefix?: string;
|
secretPrefix?: string;
|
||||||
secretSuffix?: string;
|
secretSuffix?: string;
|
||||||
|
@@ -208,20 +208,20 @@ export const kmsServiceFactory = ({
|
|||||||
return org.kmsDefaultKeyId;
|
return org.kmsDefaultKeyId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptWithRootKey = async () => {
|
const encryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
return ({ plainText }: { plainText: Buffer }) => {
|
|
||||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
|
|
||||||
|
|
||||||
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
|
return (plainTextBuffer: Buffer) => {
|
||||||
|
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
return encryptedBuffer;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptWithRootKey = async () => {
|
const decryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
|
|
||||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
|
return (cipherTextBuffer: Buffer) => {
|
||||||
return Promise.resolve(decryptedBlob);
|
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
|
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
|
||||||
|
|
||||||
@@ -19,6 +19,11 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.UserEncryptionKey}.userId`,
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
`${TableName.Users}.id`
|
`${TableName.Users}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.OrgMembership),
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||||
@@ -33,19 +38,66 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("isEmailVerified").withSchema(TableName.Users),
|
db.ref("isEmailVerified").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),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
)
|
)
|
||||||
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
|
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!member) return undefined;
|
if (!member) return undefined;
|
||||||
|
|
||||||
const { email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data } = member;
|
const doc = sqlNestRelationships({
|
||||||
|
data: member,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: ({
|
||||||
|
email,
|
||||||
|
isEmailVerified,
|
||||||
|
username,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
userId,
|
||||||
|
publicKey,
|
||||||
|
roleId,
|
||||||
|
orgId,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
isActive,
|
||||||
|
inviteEmail
|
||||||
|
}) => ({
|
||||||
|
roleId,
|
||||||
|
orgId,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
isActive,
|
||||||
|
inviteEmail,
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
isEmailVerified,
|
||||||
|
username,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
userId,
|
||||||
|
publicKey
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return doc?.[0];
|
||||||
...data,
|
|
||||||
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find org membership by id" });
|
throw new DatabaseError({ error, name: "Find org membership by id" });
|
||||||
}
|
}
|
||||||
|
@@ -29,13 +29,37 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// special query
|
// special query
|
||||||
const findAllOrgsByUserId = async (userId: string): Promise<TOrganizations[]> => {
|
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
|
||||||
try {
|
try {
|
||||||
const org = await db
|
const org = (await db
|
||||||
.replicaNode()(TableName.OrgMembership)
|
.replicaNode()(TableName.OrgMembership)
|
||||||
.where({ userId })
|
.where({ userId })
|
||||||
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||||
.select(selectAllTableCols(TableName.Organization));
|
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||||
|
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||||
|
`${TableName.SamlConfig}.isActive`,
|
||||||
|
"=",
|
||||||
|
db.raw("true")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.leftJoin(TableName.OidcConfig, (qb) => {
|
||||||
|
qb.on(`${TableName.OidcConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||||
|
`${TableName.OidcConfig}.isActive`,
|
||||||
|
"=",
|
||||||
|
db.raw("true")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.Organization))
|
||||||
|
.select(
|
||||||
|
db.raw(`
|
||||||
|
CASE
|
||||||
|
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN 'saml'
|
||||||
|
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN 'oidc'
|
||||||
|
ELSE ''
|
||||||
|
END as "orgAuthMethod"
|
||||||
|
`)
|
||||||
|
)) as (TOrganizations & { orgAuthMethod: string })[];
|
||||||
|
|
||||||
return org;
|
return org;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find all org by user id" });
|
throw new DatabaseError({ error, name: "Find all org by user id" });
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
import { TProjects } from "@app/db/schemas/projects";
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||||
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";
|
||||||
@@ -37,6 +38,7 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
|||||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
|
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
@@ -72,14 +74,16 @@ type TOrgServiceFactoryDep = {
|
|||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
groupDAL: TGroupDALFactory;
|
groupDAL: TGroupDALFactory;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||||
projectMembershipDAL: Pick<
|
projectMembershipDAL: Pick<
|
||||||
TProjectMembershipDALFactory,
|
TProjectMembershipDALFactory,
|
||||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||||
>;
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
|
||||||
incidentContactDAL: TIncidentContactsDALFactory;
|
incidentContactDAL: TIncidentContactsDALFactory;
|
||||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||||
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "findEnforceableOidcCfg">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
@@ -114,8 +118,10 @@ export const orgServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
|
oidcConfigDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectUserMembershipRoleDAL
|
projectUserMembershipRoleDAL,
|
||||||
|
identityMetadataDAL
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
@@ -266,10 +272,9 @@ export const orgServiceFactory = ({
|
|||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
if (authEnforced !== undefined) {
|
if (authEnforced !== undefined) {
|
||||||
if (!plan?.samlSSO)
|
if (!plan?.samlSSO || !plan.oidcSSO)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message: "Failed to enforce/un-enforce SSO due to plan restriction. Upgrade plan to enforce/un-enforce SSO."
|
||||||
"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);
|
||||||
}
|
}
|
||||||
@@ -285,9 +290,11 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
if (authEnforced) {
|
if (authEnforced) {
|
||||||
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
||||||
if (!samlCfg)
|
const oidcCfg = await oidcConfigDAL.findEnforceableOidcCfg(orgId);
|
||||||
|
|
||||||
|
if (!samlCfg && !oidcCfg)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "No enforceable SAML config found"
|
message: "No enforceable SSO config found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,20 +411,22 @@ export const orgServiceFactory = ({
|
|||||||
userId,
|
userId,
|
||||||
membershipId,
|
membershipId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
metadata
|
||||||
}: TUpdateOrgMembershipDTO) => {
|
}: TUpdateOrgMembershipDTO) => {
|
||||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
const foundMembership = await orgMembershipDAL.findOne({
|
const foundMembership = await orgMembershipDAL.findById(membershipId);
|
||||||
id: membershipId,
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
|
if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
|
||||||
|
if (foundMembership.orgId !== orgId)
|
||||||
|
throw new UnauthorizedError({ message: "Updated org member doesn't belong to the organization" });
|
||||||
if (foundMembership.userId === userId)
|
if (foundMembership.userId === userId)
|
||||||
throw new UnauthorizedError({ message: "Cannot update own organization membership" });
|
throw new UnauthorizedError({ message: "Cannot update own organization membership" });
|
||||||
|
|
||||||
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||||
|
let userRole = role;
|
||||||
|
let userRoleId: string | null = null;
|
||||||
if (role && isCustomRole) {
|
if (role && isCustomRole) {
|
||||||
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||||
if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" });
|
if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" });
|
||||||
@@ -428,17 +437,31 @@ export const orgServiceFactory = ({
|
|||||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||||
});
|
});
|
||||||
|
|
||||||
const [membership] = await orgDAL.updateMembership(
|
userRole = OrgMembershipRole.Custom;
|
||||||
{ id: membershipId, orgId },
|
userRoleId = customRole.id;
|
||||||
{
|
|
||||||
role: OrgMembershipRole.Custom,
|
|
||||||
roleId: customRole.id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return membership;
|
|
||||||
}
|
}
|
||||||
|
const membership = await orgDAL.transaction(async (tx) => {
|
||||||
|
const [updatedOrgMembership] = await orgDAL.updateMembership(
|
||||||
|
{ id: membershipId, orgId },
|
||||||
|
{ role: userRole, roleId: userRoleId, isActive }
|
||||||
|
);
|
||||||
|
|
||||||
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null, isActive });
|
if (metadata) {
|
||||||
|
await identityMetadataDAL.delete({ userId: updatedOrgMembership.userId, orgId }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
userId: updatedOrgMembership.userId,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedOrgMembership;
|
||||||
|
});
|
||||||
return membership;
|
return membership;
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
|
@@ -9,6 +9,7 @@ export type TUpdateOrgMembershipDTO = {
|
|||||||
role?: string;
|
role?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
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 { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -16,6 +18,7 @@ type TProjectEnvServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
|
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
|
||||||
@@ -24,6 +27,7 @@ export const projectEnvServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
keyStore,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
}: TProjectEnvServiceFactoryDep) => {
|
}: TProjectEnvServiceFactoryDep) => {
|
||||||
@@ -45,32 +49,56 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const envs = await projectEnvDAL.find({ projectId });
|
const lock = await keyStore
|
||||||
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
|
||||||
if (existingEnv)
|
.catch(() => null);
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Environment with slug already exist",
|
try {
|
||||||
name: "CreateEnvironment"
|
if (!lock) {
|
||||||
|
await keyStore.waitTillReady({
|
||||||
|
key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
keyCheckCb: (val) => val === "true",
|
||||||
|
waitingCb: () => logger.debug("Create project environment. Waiting for "),
|
||||||
|
delay: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envs = await projectEnvDAL.find({ projectId });
|
||||||
|
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
||||||
|
if (existingEnv)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Environment with slug already exist",
|
||||||
|
name: "CreateEnvironment"
|
||||||
|
});
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
const plan = await licenseService.getPlan(project.orgId);
|
||||||
|
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
|
||||||
|
// case: limit imposed on number of environments allowed
|
||||||
|
// case: number of environments used exceeds the number of environments allowed
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
|
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
||||||
|
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
|
||||||
|
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
||||||
|
return doc;
|
||||||
});
|
});
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
await keyStore.setItemWithExpiry(
|
||||||
const plan = await licenseService.getPlan(project.orgId);
|
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
|
10,
|
||||||
// case: limit imposed on number of environments allowed
|
"true"
|
||||||
// case: number of environments used exceeds the number of environments allowed
|
);
|
||||||
throw new BadRequestError({
|
|
||||||
message:
|
return env;
|
||||||
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
} finally {
|
||||||
});
|
await lock?.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
|
||||||
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
|
||||||
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
|
|
||||||
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
|
||||||
return doc;
|
|
||||||
});
|
|
||||||
return env;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEnvironment = async ({
|
const updateEnvironment = async ({
|
||||||
@@ -93,26 +121,50 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
|
const lock = await keyStore
|
||||||
if (!oldEnv) throw new NotFoundError({ message: "Environment not found" });
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
if (slug) {
|
try {
|
||||||
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
if (!lock) {
|
||||||
if (existingEnv && existingEnv.id !== id) {
|
await keyStore.waitTillReady({
|
||||||
throw new BadRequestError({
|
key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
message: "Environment with slug already exist",
|
keyCheckCb: (val) => val === "true",
|
||||||
name: "UpdateEnvironment"
|
waitingCb: () => logger.debug("Update project environment. Waiting for project environment update"),
|
||||||
|
delay: 500
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
|
||||||
if (position) {
|
if (!oldEnv) throw new NotFoundError({ message: "Environment not found", name: "UpdateEnvironment" });
|
||||||
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
|
|
||||||
|
if (slug) {
|
||||||
|
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
||||||
|
if (existingEnv && existingEnv.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Environment with slug already exist",
|
||||||
|
name: "UpdateEnvironment"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
|
|
||||||
});
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
return { environment: env, old: oldEnv };
|
if (position) {
|
||||||
|
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
|
||||||
|
}
|
||||||
|
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
10,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
|
||||||
|
return { environment: env, old: oldEnv };
|
||||||
|
} finally {
|
||||||
|
await lock?.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
||||||
@@ -125,18 +177,42 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
const lock = await keyStore
|
||||||
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentLock(projectId)], 5000)
|
||||||
if (!doc)
|
.catch(() => null);
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Env doesn't exist",
|
|
||||||
name: "DeleteEnvironment"
|
|
||||||
});
|
|
||||||
|
|
||||||
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
|
try {
|
||||||
return doc;
|
if (!lock) {
|
||||||
});
|
await keyStore.waitTillReady({
|
||||||
return env;
|
key: KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
keyCheckCb: (val) => val === "true",
|
||||||
|
waitingCb: () => logger.debug("Delete project environment. Waiting for "),
|
||||||
|
delay: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
|
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
|
||||||
|
if (!doc)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Environment doesn't exist",
|
||||||
|
name: "DeleteEnvironment"
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.WaitUntilReadyProjectEnvironmentOperation(projectId),
|
||||||
|
10,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
|
||||||
|
return env;
|
||||||
|
} finally {
|
||||||
|
await lock?.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
|
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
|
||||||
|
@@ -7,8 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSet,
|
ProjectPermissionSet,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub
|
||||||
validateProjectPermissions
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
@@ -60,8 +59,6 @@ export const projectRoleServiceFactory = ({
|
|||||||
throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" });
|
throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateProjectPermissions(data.permissions);
|
|
||||||
|
|
||||||
const role = await projectRoleDAL.create({
|
const role = await projectRoleDAL.create({
|
||||||
...data,
|
...data,
|
||||||
projectId
|
projectId
|
||||||
@@ -127,10 +124,6 @@ export const projectRoleServiceFactory = ({
|
|||||||
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
|
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.permissions) {
|
|
||||||
validateProjectPermissions(data.permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [updatedRole] = await projectRoleDAL.update(
|
const [updatedRole] = await projectRoleDAL.update(
|
||||||
{ id: roleId, projectId },
|
{ id: roleId, projectId },
|
||||||
{
|
{
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, TProjectEnvironments } 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";
|
||||||
@@ -146,7 +146,8 @@ export const projectServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
slug: projectSlug,
|
slug: projectSlug,
|
||||||
kmsKeyId
|
kmsKeyId,
|
||||||
|
createDefaultEnvs = true
|
||||||
}: TCreateProjectDTO) => {
|
}: TCreateProjectDTO) => {
|
||||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||||
|
|
||||||
@@ -207,14 +208,17 @@ export const projectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// set default environments and root folder for provided environments
|
// set default environments and root folder for provided environments
|
||||||
const envs = await projectEnvDAL.insertMany(
|
let envs: TProjectEnvironments[] = [];
|
||||||
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
|
if (createDefaultEnvs) {
|
||||||
tx
|
envs = await projectEnvDAL.insertMany(
|
||||||
);
|
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
|
||||||
await folderDAL.insertMany(
|
tx
|
||||||
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
);
|
||||||
tx
|
await folderDAL.insertMany(
|
||||||
);
|
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Create a random key that we'll use as the project key.
|
// 3. Create a random key that we'll use as the project key.
|
||||||
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
||||||
|
@@ -29,6 +29,7 @@ export type TCreateProjectDTO = {
|
|||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
|
createDefaultEnvs?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteProjectBySlugDTO = {
|
export type TDeleteProjectBySlugDTO = {
|
||||||
|
@@ -548,7 +548,7 @@ export const secretImportServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TSecretSharing } from "@app/db/schemas";
|
import { TSecretSharing } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { SecretSharingAccessType } from "@app/lib/types";
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
import {
|
import {
|
||||||
@@ -19,14 +23,18 @@ type TSecretSharingServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
secretSharingDAL: TSecretSharingDALFactory;
|
secretSharingDAL: TSecretSharingDALFactory;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
|
kmsService: TKmsServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||||
|
|
||||||
|
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
|
||||||
|
|
||||||
export const secretSharingServiceFactory = ({
|
export const secretSharingServiceFactory = ({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL
|
orgDAL,
|
||||||
|
kmsService
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const createSharedSecret = async ({
|
const createSharedSecret = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -34,10 +42,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
encryptedValue,
|
secretValue,
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
accessType,
|
accessType,
|
||||||
@@ -59,19 +64,25 @@ export const secretSharingServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
if (secretValue.length > 10_000) {
|
||||||
if (encryptedValue.length > 13000) {
|
|
||||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||||
|
|
||||||
|
const id = crypto.randomBytes(32).toString("hex");
|
||||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
identifier: id,
|
||||||
|
iv: null,
|
||||||
|
tag: null,
|
||||||
|
encryptedValue: null,
|
||||||
|
encryptedSecret,
|
||||||
name,
|
name,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
encryptedValue,
|
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
@@ -79,15 +90,14 @@ export const secretSharingServiceFactory = ({
|
|||||||
accessType
|
accessType
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: newSharedSecret.id };
|
const idToReturn = `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}`;
|
||||||
|
|
||||||
|
return { id: idToReturn };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPublicSharedSecret = async ({
|
const createPublicSharedSecret = async ({
|
||||||
password,
|
password,
|
||||||
encryptedValue,
|
secretValue,
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
@@ -104,24 +114,25 @@ export const secretSharingServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
if (encryptedValue.length > 13000) {
|
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const id = crypto.randomBytes(32).toString("hex");
|
||||||
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
|
||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
identifier: id,
|
||||||
|
encryptedValue: null,
|
||||||
|
iv: null,
|
||||||
|
tag: null,
|
||||||
|
encryptedSecret,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
encryptedValue,
|
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: newSharedSecret.id };
|
return { id: `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}` };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSharedSecrets = async ({
|
const getSharedSecrets = async ({
|
||||||
@@ -162,25 +173,30 @@ export const secretSharingServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
|
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing) => {
|
||||||
const { expiresAfterViews } = sharedSecret;
|
const { expiresAfterViews } = sharedSecret;
|
||||||
|
|
||||||
if (expiresAfterViews) {
|
if (expiresAfterViews) {
|
||||||
// decrement view count if view count expiry set
|
// decrement view count if view count expiry set
|
||||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
await secretSharingDAL.updateById(sharedSecret.id, { $decr: { expiresAfterViews: 1 } });
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretSharingDAL.updateById(sharedSecretId, {
|
await secretSharingDAL.updateById(sharedSecret.id, {
|
||||||
lastViewedAt: new Date()
|
lastViewedAt: new Date()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
|
/** Get's password-less secret. validates all secret's requested (must be fresh). */
|
||||||
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
|
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({
|
const sharedSecret = isUuidV4(sharedSecretId)
|
||||||
id: sharedSecretId,
|
? await secretSharingDAL.findOne({
|
||||||
hashedHex
|
id: sharedSecretId,
|
||||||
});
|
hashedHex
|
||||||
|
})
|
||||||
|
: await secretSharingDAL.findOne({
|
||||||
|
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
|
||||||
|
});
|
||||||
|
|
||||||
if (!sharedSecret)
|
if (!sharedSecret)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Shared secret not found"
|
message: "Shared secret not found"
|
||||||
@@ -222,13 +238,23 @@ export const secretSharingServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If encryptedSecret is set, we know that this secret has been encrypted using KMS, and we can therefore do server-side decryption.
|
||||||
|
let decryptedSecretValue: Buffer | undefined;
|
||||||
|
if (sharedSecret.encryptedSecret) {
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
decryptedSecretValue = decryptWithRoot(sharedSecret.encryptedSecret);
|
||||||
|
}
|
||||||
|
|
||||||
// decrement when we are sure the user will view secret.
|
// decrement when we are sure the user will view secret.
|
||||||
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
|
await $decrementSecretViewCount(sharedSecret);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isPasswordProtected,
|
isPasswordProtected,
|
||||||
secret: {
|
secret: {
|
||||||
...sharedSecret,
|
...sharedSecret,
|
||||||
|
...(decryptedSecretValue && {
|
||||||
|
secretValue: Buffer.from(decryptedSecretValue).toString()
|
||||||
|
}),
|
||||||
orgName:
|
orgName:
|
||||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||||
? orgName
|
? orgName
|
||||||
@@ -241,7 +267,16 @@ export const secretSharingServiceFactory = ({
|
|||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
||||||
|
|
||||||
|
const sharedSecret = isUuidV4(sharedSecretId)
|
||||||
|
? await secretSharingDAL.findById(sharedSecretId)
|
||||||
|
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
|
||||||
|
|
||||||
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
|
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
|
||||||
|
|
||||||
|
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
|
||||||
|
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
|
||||||
|
|
||||||
return deletedSharedSecret;
|
return deletedSharedSecret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,10 +19,7 @@ export type TSharedSecretPermission = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TCreatePublicSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
encryptedValue: string;
|
secretValue: string;
|
||||||
hashedHex: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
expiresAfterViews?: number;
|
expiresAfterViews?: number;
|
||||||
password?: string;
|
password?: string;
|
||||||
@@ -31,7 +28,7 @@ export type TCreatePublicSharedSecretDTO = {
|
|||||||
|
|
||||||
export type TGetActiveSharedSecretByIdDTO = {
|
export type TGetActiveSharedSecretByIdDTO = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
hashedHex: string;
|
hashedHex?: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
|
@@ -835,7 +835,7 @@ export const createManySecretsRawFnFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
const inputSecrets = secrets.map((secret) => {
|
const inputSecrets = secrets.map((secret) => {
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
||||||
@@ -1000,7 +1000,7 @@ export const updateManySecretsRawFnFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||||
if (!blindIndexCfg) throw new NotFoundError({ message: "Blind index not found", name: "Update secret" });
|
if (!blindIndexCfg) throw new NotFoundError({ message: "Blind index not found", name: "Update secret" });
|
||||||
|
@@ -930,6 +930,11 @@ export const secretQueueFactory = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// re-throw error to re-run job unless final attempt, then log and send fail email
|
||||||
|
if (job.attemptsStarted !== job.opts.attempts) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
await integrationDAL.updateById(integration.id, {
|
await integrationDAL.updateById(integration.id, {
|
||||||
lastSyncJobId: job.id,
|
lastSyncJobId: job.id,
|
||||||
syncMessage: message,
|
syncMessage: message,
|
||||||
|
@@ -1105,7 +1105,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
const { secrets, imports } = await getSecrets({
|
const { secrets, imports } = await getSecrets({
|
||||||
@@ -1269,7 +1269,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
||||||
|
|
||||||
@@ -1365,7 +1365,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||||
@@ -1507,7 +1507,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||||
@@ -1633,7 +1633,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
if (policy) {
|
if (policy) {
|
||||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||||
@@ -1737,7 +1737,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
const sanitizedSecrets = inputSecrets.map(
|
const sanitizedSecrets = inputSecrets.map(
|
||||||
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
||||||
@@ -1863,7 +1863,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
const sanitizedSecrets = inputSecrets.map(
|
const sanitizedSecrets = inputSecrets.map(
|
||||||
({
|
({
|
||||||
@@ -1995,7 +1995,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (policy) {
|
if (policy) {
|
||||||
@@ -2332,7 +2332,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey)
|
if (!botKey)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
await secretDAL.transaction(async (tx) => {
|
await secretDAL.transaction(async (tx) => {
|
||||||
@@ -2418,7 +2418,7 @@ export const secretServiceFactory = ({
|
|||||||
if (!botKey) {
|
if (!botKey) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Project bot not found. Please upgrade your project.",
|
message: "Project bot not found. Please upgrade your project.",
|
||||||
name: "BotNotFoundError"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -141,16 +141,14 @@ export const slackServiceFactory = ({
|
|||||||
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
|
||||||
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
|
||||||
|
|
||||||
const decrypt = await kmsService.decryptWithRootKey();
|
const decrypt = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientId) {
|
if (serverCfg.encryptedSlackClientId) {
|
||||||
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString();
|
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientSecret) {
|
if (serverCfg.encryptedSlackClientSecret) {
|
||||||
slackClientSecret = (
|
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
|
||||||
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
|
|
||||||
).toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!slackClientId || !slackClientSecret) {
|
if (!slackClientId || !slackClientSecret) {
|
||||||
|
@@ -122,20 +122,16 @@ export const superAdminServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptWithRoot = await kmsService.encryptWithRootKey();
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
if (data.slackClientId) {
|
if (data.slackClientId) {
|
||||||
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({
|
const encryptedClientId = encryptWithRoot(Buffer.from(data.slackClientId));
|
||||||
plainText: Buffer.from(data.slackClientId)
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedData.encryptedSlackClientId = encryptedClientId;
|
updatedData.encryptedSlackClientId = encryptedClientId;
|
||||||
updatedData.slackClientId = undefined;
|
updatedData.slackClientId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.slackClientSecret) {
|
if (data.slackClientSecret) {
|
||||||
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({
|
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.slackClientSecret));
|
||||||
plainText: Buffer.from(data.slackClientSecret)
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
|
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
|
||||||
updatedData.slackClientSecret = undefined;
|
updatedData.slackClientSecret = undefined;
|
||||||
@@ -270,14 +266,14 @@ export const superAdminServiceFactory = ({
|
|||||||
let clientId = "";
|
let clientId = "";
|
||||||
let clientSecret = "";
|
let clientSecret = "";
|
||||||
|
|
||||||
const decrypt = await kmsService.decryptWithRootKey();
|
const decrypt = kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientId) {
|
if (serverCfg.encryptedSlackClientId) {
|
||||||
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString();
|
clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverCfg.encryptedSlackClientSecret) {
|
if (serverCfg.encryptedSlackClientSecret) {
|
||||||
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString();
|
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -12,11 +12,29 @@ Infisical is used by 10,000+ organizations across all industries including First
|
|||||||
|
|
||||||
## Migrating from EnvKey
|
## Migrating from EnvKey
|
||||||
|
|
||||||
To facilitate customer transition from EnvKey to Infisical, we have been working closely with the EnvKey team to provide a simple migration path for all EnvKey customers.
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
Open the EnvKey dashboard and go to My Org.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
Go to Import/Export on the top right corner, Click on Export Org and save the exported file.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
Click on copy to copy the encryption key and save it.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
Open the Infisical dashboard and go to Organization Settings > Import.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
Upload the exported file from EnvKey, paste the encryption key and click Import.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
## Automated migration
|
|
||||||
|
|
||||||
Our team is currently working on creating an automated migration process that would include secrets, policies, and other important resources. If you are interested in that, please [reach out to our team](mailto:support@infisical.com) with any questions.
|
|
||||||
|
|
||||||
## Talk to our team
|
## Talk to our team
|
||||||
|
|
||||||
|
168
docs/documentation/platform/dynamic-secrets/ldap.mdx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
title: "LDAP"
|
||||||
|
description: "Learn how to dynamically generate user credentials via LDAP."
|
||||||
|
---
|
||||||
|
|
||||||
|
The Infisical LDAP dynamic secret allows you to generate user credentials on demand via LDAP. The integration is general to any LDAP implementation but has been tested with OpenLDAP and Active directory as of now.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Create a user with the necessary permissions to create users in your LDAP server.
|
||||||
|
2. Ensure your LDAP server is reachable via Infisical instance.
|
||||||
|
|
||||||
|
## Set up Dynamic Secrets with LDAP
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open Secret Overview Dashboard">
|
||||||
|
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select 'LDAP'">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
Name by which you want the secret to be referenced
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Default TTL" type="string" required>
|
||||||
|
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Max TTL" type="string" required>
|
||||||
|
Maximum time-to-live for a generated secret.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="URL" type="string" required>
|
||||||
|
LDAP url to connect to. _(Example: ldap://your-ldap-ip:389 or ldaps://domain:636)_
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="BIND DN" type="string" required>
|
||||||
|
DN to bind to. This should have permissions to create a new users.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="BIND Password" type="string" required>
|
||||||
|
Password for the given DN.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="text">
|
||||||
|
CA certificate to use for TLS in case of a secure connection.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Creation LDIF" type="text" required>
|
||||||
|
LDIF to run while creating a user in LDAP. This can include extra steps to assign the user to groups or set permissions.
|
||||||
|
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret.
|
||||||
|
|
||||||
|
`{{EncodedPassword}}` is the encoded password required for the `unicodePwd` field in Active Directory as described [here](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password).
|
||||||
|
|
||||||
|
**OpenLDAP** Example:
|
||||||
|
```
|
||||||
|
dn: uid={{Username}},dc=infisical,dc=com
|
||||||
|
changetype: add
|
||||||
|
objectClass: top
|
||||||
|
objectClass: person
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: John Doe
|
||||||
|
sn: Doe
|
||||||
|
uid: jdoe
|
||||||
|
mail: jdoe@infisical.com
|
||||||
|
userPassword: {{Password}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Active Directory** Example:
|
||||||
|
```
|
||||||
|
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: add
|
||||||
|
objectClass: top
|
||||||
|
objectClass: person
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
objectClass: user
|
||||||
|
userPrincipalName: {{Username}}@infisical.com
|
||||||
|
sAMAccountName: {{Username}}
|
||||||
|
unicodePwd::{{EncodedPassword}}
|
||||||
|
userAccountControl: 66048
|
||||||
|
|
||||||
|
dn: CN=test-group,OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: modify
|
||||||
|
add: member
|
||||||
|
member: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
-
|
||||||
|
```
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Revocation LDIF" type="text" required>
|
||||||
|
LDIF to run while revoking a user in LDAP. This can include extra steps to remove the user from groups or set permissions.
|
||||||
|
Here `{{Username}}` is a templatized variable for the username generated by the dynamic secret.
|
||||||
|
|
||||||
|
**OpenLDAP / Active Directory** Example:
|
||||||
|
```
|
||||||
|
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: delete
|
||||||
|
```
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Rollback LDIF" type="text">
|
||||||
|
LDIF to run incase Creation LDIF fails midway.
|
||||||
|
|
||||||
|
For the creation example shown above, if the user is created successfully but not added to a group, this LDIF can be used to remove the user.
|
||||||
|
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username generated by the dynamic secret.
|
||||||
|
|
||||||
|
**OpenLDAP / Active Directory** Example:
|
||||||
|
```
|
||||||
|
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
|
||||||
|
changetype: delete
|
||||||
|
```
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Click `Submit`">
|
||||||
|
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||||
|
</Step>
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||||
|
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||||
|
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Active Directory Integration
|
||||||
|
|
||||||
|
- Passwords in Active Directory are set using the `unicodePwd` field. This must be proceeded by two colons `::` as shown in the example. [Source](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password)
|
||||||
|
- Active directory uses the `userAccountControl` field to enable account. [Read More](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/useraccountcontrol-manipulate-account-properties)
|
||||||
|
- `userAccountControl` set to `512` enables a user.
|
||||||
|
- To disable AD's password expiration for this dynamic user account. The `userAccountControl` value for this is: `65536`.
|
||||||
|
- Since `userAccountControl` flag is cumulative set it to `512 + 65536` = `66048` to do both.
|
||||||
|
- Active Directory does not permit direct modification of a user's `memberOf` attribute. The member attribute of a group and the `memberOf` attribute of a user are [linked attributes](https://learn.microsoft.com/en-us/windows/win32/ad/linked-attributes), where the member attribute represents the forward link, which can be modified. In the context of AD group membership, the group's `member` attribute serves as the forward link. Therefore, to add a newly created dynamic user to a group, a modification request must be issued to the desired group, updating its membership to include the new user.
|
||||||
|
|
||||||
|
## LDIF Entries
|
||||||
|
|
||||||
|
User account management is handled through **LDIF entries**.
|
||||||
|
|
||||||
|
#### Things to Remember
|
||||||
|
|
||||||
|
- **No trailing spaces:** Ensure there are no trailing spaces on any line, including blank lines.
|
||||||
|
- **Empty lines before modify blocks:** Every modify block must be preceded by an empty line.
|
||||||
|
- **Multiple modifications:** You can define multiple modifications for a DN within a single modify block. Each modification should end with a single dash (`-`).
|
@@ -39,7 +39,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
|
|||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Finish configuring OIDC in Infisical">
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Manage**.
|
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**.
|
||||||

|

|
||||||
|
|
||||||
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
|
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
|
||||||
@@ -54,6 +54,19 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
|
|||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
|
||||||
|
by logging into the organization via Auth0.
|
||||||
|
|
||||||
|
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Auth0 user with Infisical.
|
||||||
|
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the application in Auth0
|
||||||
|
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
@@ -28,7 +28,7 @@ Prerequisites:
|
|||||||
1.4. Access the IdP’s OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
|
1.4. Access the IdP’s OIDC discovery document (usually located at `https://<idp-domain>/.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Finish configuring OIDC in Infisical">
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage
|
2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
||||||

|

|
||||||
|
|
||||||
2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints.
|
2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints.
|
||||||
@@ -53,9 +53,20 @@ Prerequisites:
|
|||||||
<Step title="Enable OIDC SSO in Infisical">
|
<Step title="Enable OIDC SSO in Infisical">
|
||||||
Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider
|
Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider
|
||||||
|
|
||||||

|

|
||||||
|
</Step>
|
||||||
|
|
||||||
</Step>
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
|
||||||
|
by logging into the organization via the Identity provider.
|
||||||
|
|
||||||
|
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one IdP user with Infisical.
|
||||||
|
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
@@ -65,7 +65,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
|||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Finish configuring OIDC in Infisical">
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage
|
3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect.
|
||||||

|

|
||||||
|
|
||||||
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**.
|
||||||
@@ -80,6 +80,19 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
|||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
|
||||||
|
by logging into the organization via Keycloak.
|
||||||
|
|
||||||
|
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one Keycloak user with Infisical.
|
||||||
|
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the application in Keycloak
|
||||||
|
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
BIN
docs/images/guides/import-envkey/copy-encryption-key.png
Normal file
After Width: | Height: | Size: 403 KiB |
BIN
docs/images/guides/import-envkey/envkey-dashboard.png
Normal file
After Width: | Height: | Size: 216 KiB |
BIN
docs/images/guides/import-envkey/envkey-export.png
Normal file
After Width: | Height: | Size: 413 KiB |
BIN
docs/images/guides/import-envkey/infisical-import-dashboard.png
Normal file
After Width: | Height: | Size: 896 KiB |
BIN
docs/images/guides/import-envkey/infisical-import-envkey.png
Normal file
After Width: | Height: | Size: 609 KiB |
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 490 KiB |
Before Width: | Height: | Size: 754 KiB After Width: | Height: | Size: 797 KiB |
Before Width: | Height: | Size: 746 KiB After Width: | Height: | Size: 780 KiB |
Before Width: | Height: | Size: 743 KiB After Width: | Height: | Size: 797 KiB |
Before Width: | Height: | Size: 741 KiB After Width: | Height: | Size: 780 KiB |
Before Width: | Height: | Size: 751 KiB After Width: | Height: | Size: 797 KiB |
Before Width: | Height: | Size: 744 KiB After Width: | Height: | Size: 780 KiB |
@@ -168,7 +168,8 @@
|
|||||||
"documentation/platform/dynamic-secrets/aws-iam",
|
"documentation/platform/dynamic-secrets/aws-iam",
|
||||||
"documentation/platform/dynamic-secrets/mongo-atlas",
|
"documentation/platform/dynamic-secrets/mongo-atlas",
|
||||||
"documentation/platform/dynamic-secrets/mongo-db",
|
"documentation/platform/dynamic-secrets/mongo-db",
|
||||||
"documentation/platform/dynamic-secrets/azure-entra-id"
|
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||||
|
"documentation/platform/dynamic-secrets/ldap"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
141
frontend/src/components/navigation/RegionSelect.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
|
||||||
|
|
||||||
|
enum Region {
|
||||||
|
US = "us",
|
||||||
|
EU = "eu"
|
||||||
|
}
|
||||||
|
|
||||||
|
const regions = [
|
||||||
|
{
|
||||||
|
value: Region.US,
|
||||||
|
label: "United States",
|
||||||
|
location: "Virginia, USA",
|
||||||
|
flag: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 640 480">
|
||||||
|
<path fill="#bd3d44" d="M0 0h640v480H0" />
|
||||||
|
<path
|
||||||
|
stroke="#fff"
|
||||||
|
strokeWidth="37"
|
||||||
|
d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"
|
||||||
|
/>
|
||||||
|
<path fill="#192f5d" d="M0 0h364.8v258.5H0" />
|
||||||
|
<marker id="us-a" markerHeight="30" markerWidth="30">
|
||||||
|
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z" />
|
||||||
|
</marker>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
markerMid="url(#us-a)"
|
||||||
|
d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Region.EU,
|
||||||
|
label: "Europe",
|
||||||
|
location: "Frankfurt, Germany",
|
||||||
|
flag: (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-eu" viewBox="0 0 512 512">
|
||||||
|
<defs>
|
||||||
|
<g id="eu-d">
|
||||||
|
<g id="eu-b">
|
||||||
|
<path id="eu-a" d="m0-1-.3 1 .5.1z" />
|
||||||
|
<use xlinkHref="#eu-a" transform="scale(-1 1)" />
|
||||||
|
</g>
|
||||||
|
<g id="eu-c">
|
||||||
|
<use xlinkHref="#eu-b" transform="rotate(72)" />
|
||||||
|
<use xlinkHref="#eu-b" transform="rotate(144)" />
|
||||||
|
</g>
|
||||||
|
<use xlinkHref="#eu-c" transform="scale(-1 1)" />
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<path fill="#039" d="M0 0h512v512H0z" />
|
||||||
|
<g fill="#fc0" transform="translate(256 258.4)scale(25.28395)">
|
||||||
|
<use xlinkHref="#eu-d" width="100%" height="100%" y="-6" />
|
||||||
|
<use xlinkHref="#eu-d" width="100%" height="100%" y="6" />
|
||||||
|
<g id="eu-e">
|
||||||
|
<use xlinkHref="#eu-d" width="100%" height="100%" x="-6" />
|
||||||
|
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(-144 -2.3 -2.1)" />
|
||||||
|
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(144 -2.1 -2.3)" />
|
||||||
|
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(72 -4.7 -2)" />
|
||||||
|
<use xlinkHref="#eu-d" width="100%" height="100%" transform="rotate(72 -5 .5)" />
|
||||||
|
</g>
|
||||||
|
<use xlinkHref="#eu-e" width="100%" height="100%" transform="scale(-1 1)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RegionSelect = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleRegionSelect = (value: Region) => {
|
||||||
|
router.push(`https://${value}.infisical.com/${router.pathname}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [subdomain, domain] = window.location.host.split(".");
|
||||||
|
|
||||||
|
// only display region select for cloud
|
||||||
|
if (!domain?.match(/infisical/)) return null;
|
||||||
|
|
||||||
|
// default to US if not eu
|
||||||
|
const currentRegion = subdomain === Region.EU ? regions[1] : regions[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8 flex flex-col items-center">
|
||||||
|
<Select
|
||||||
|
className="w-44"
|
||||||
|
onValueChange={handleRegionSelect}
|
||||||
|
defaultValue={currentRegion.value}
|
||||||
|
>
|
||||||
|
{regions.map(({ value, label, flag }) => (
|
||||||
|
<SelectItem value={value} key={value}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4">{flag}</div>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Modal>
|
||||||
|
<ModalTrigger>
|
||||||
|
<button type="button" className="mt-1 text-right text-xs text-mineshaft-400 underline">
|
||||||
|
Help me pick a data region
|
||||||
|
</button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="Infisical Cloud data regions"
|
||||||
|
subTitle="Select the closest region to you and your team. Contact Infisical if you need to migrate regions."
|
||||||
|
>
|
||||||
|
{regions.map(({ value, label, location, flag }) => (
|
||||||
|
<div className="mb-6" key={value}>
|
||||||
|
<p className="font-medium">
|
||||||
|
<span className="mr-2 inline-block w-4">{flag}</span>
|
||||||
|
{value.toUpperCase()} Region
|
||||||
|
</p>
|
||||||
|
<ul className="ml-6 mt-2 flex flex-col gap-1">
|
||||||
|
<li>
|
||||||
|
<FontAwesomeIcon size="xs" className="mr-0.5 text-green" icon={faCheck} /> Fastest
|
||||||
|
option if you are based in {value === Region.US ? "the" : ""} {label}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FontAwesomeIcon size="xs" className="mr-0.5 text-green" icon={faCheck} /> Data
|
||||||
|
storage compliance for this region
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FontAwesomeIcon size="xs" className="mr-0.5 text-green" icon={faCheck} /> Hosted
|
||||||
|
in {location}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -4,6 +4,7 @@ import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons
|
|||||||
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { RegionSelect } from "@app/components/navigation/RegionSelect";
|
||||||
import { useServerConfig } from "@app/context";
|
import { useServerConfig } from "@app/context";
|
||||||
import { LoginMethod } from "@app/hooks/api/admin/types";
|
import { LoginMethod } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export default function InitialSignupStep({
|
|||||||
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||||
{t("signup.initial-title")}
|
{t("signup.initial-title")}
|
||||||
</h1>
|
</h1>
|
||||||
|
<RegionSelect />
|
||||||
{shouldDisplaySignupMethod(LoginMethod.GOOGLE) && (
|
{shouldDisplaySignupMethod(LoginMethod.GOOGLE) && (
|
||||||
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
|
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
|
||||||
<Button
|
<Button
|
||||||
|
@@ -24,7 +24,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
|||||||
{...props}
|
{...props}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"z-30 min-w-[220px] rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
"z-30 min-w-[220px] overflow-y-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 text-bunker-300 shadow will-change-auto data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@@ -77,7 +77,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
const debouncedValue = useDebounce(value, 500);
|
const [debouncedValue] = useDebounce(value, 500);
|
||||||
|
|
||||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||||
|
|
||||||
|
@@ -33,7 +33,7 @@ export const SecretPathInput = ({
|
|||||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||||
const [isInputFocused, setIsInputFocus] = useState(false);
|
const [isInputFocused, setIsInputFocus] = useState(false);
|
||||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||||
const debouncedInputValue = useDebounce(inputValue, 200);
|
const [debouncedInputValue] = useDebounce(inputValue, 200);
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
@@ -56,7 +56,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
isDisabled && "cursor-not-allowed opacity-50"
|
isDisabled && "cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{props.icon && <FontAwesomeIcon icon={props.icon} />}
|
{props.icon && <FontAwesomeIcon icon={props.icon} />}
|
||||||
<SelectPrimitive.Value placeholder={placeholder} />
|
<SelectPrimitive.Value placeholder={placeholder} />
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +72,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
|
"relative top-1 z-[100] max-w-sm overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
|
||||||
position === "popper" && "max-h-72",
|
position === "popper" && "max-h-72",
|
||||||
dropdownContainerClassName
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
@@ -122,8 +122,8 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`relative mb-0.5 flex
|
`relative mb-0.5 flex
|
||||||
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
cursor-pointer select-none items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-md py-2
|
||||||
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
|
pl-10 pr-4 text-sm outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
|
||||||
isSelected && "bg-primary",
|
isSelected && "bg-primary",
|
||||||
isDisabled &&
|
isDisabled &&
|
||||||
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
||||||
|
@@ -7,6 +7,30 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PermissionConditionOperators {
|
||||||
|
$IN = "$in",
|
||||||
|
$ALL = "$all",
|
||||||
|
$REGEX = "$regex",
|
||||||
|
$EQ = "$eq",
|
||||||
|
$NEQ = "$neq",
|
||||||
|
$GLOB = "$glob"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TPermissionConditionOperators = {
|
||||||
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
|
[PermissionConditionOperators.$ALL]: string[];
|
||||||
|
[PermissionConditionOperators.$EQ]: string;
|
||||||
|
[PermissionConditionOperators.$NEQ]: string;
|
||||||
|
[PermissionConditionOperators.$REGEX]: string;
|
||||||
|
[PermissionConditionOperators.$GLOB]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPermissionCondition = Record<
|
||||||
|
string,
|
||||||
|
| string
|
||||||
|
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string }
|
||||||
|
>;
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
|
@@ -48,21 +48,15 @@ export const dashboardKeys = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchProjectSecretsOverview = async ({
|
export const fetchProjectSecretsOverview = async ({
|
||||||
includeFolders,
|
|
||||||
includeSecrets,
|
|
||||||
includeDynamicSecrets,
|
|
||||||
environments,
|
environments,
|
||||||
...params
|
...params
|
||||||
}: TGetDashboardProjectSecretsOverviewDTO) => {
|
}: TGetDashboardProjectSecretsOverviewDTO) => {
|
||||||
const { data } = await apiRequest.get<DashboardProjectSecretsOverviewResponse>(
|
const { data } = await apiRequest.get<DashboardProjectSecretsOverviewResponse>(
|
||||||
"/api/v3/dashboard/secrets-overview",
|
"/api/v1/dashboard/secrets-overview",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
environments: encodeURIComponent(environments.join(",")),
|
environments: encodeURIComponent(environments.join(","))
|
||||||
includeFolders: includeFolders ? "1" : "",
|
|
||||||
includeSecrets: includeSecrets ? "1" : "",
|
|
||||||
includeDynamicSecrets: includeDynamicSecrets ? "1" : ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -71,22 +65,14 @@ export const fetchProjectSecretsOverview = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchProjectSecretsDetails = async ({
|
export const fetchProjectSecretsDetails = async ({
|
||||||
includeFolders,
|
|
||||||
includeImports,
|
|
||||||
includeSecrets,
|
|
||||||
includeDynamicSecrets,
|
|
||||||
tags,
|
tags,
|
||||||
...params
|
...params
|
||||||
}: TGetDashboardProjectSecretsDetailsDTO) => {
|
}: TGetDashboardProjectSecretsDetailsDTO) => {
|
||||||
const { data } = await apiRequest.get<DashboardProjectSecretsDetailsResponse>(
|
const { data } = await apiRequest.get<DashboardProjectSecretsDetailsResponse>(
|
||||||
"/api/v3/dashboard/secrets-details",
|
"/api/v1/dashboard/secrets-details",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
includeImports: includeImports ? "1" : "",
|
|
||||||
includeFolders: includeFolders ? "1" : "",
|
|
||||||
includeSecrets: includeSecrets ? "1" : "",
|
|
||||||
includeDynamicSecrets: includeDynamicSecrets ? "1" : "",
|
|
||||||
tags: encodeURIComponent(
|
tags: encodeURIComponent(
|
||||||
Object.entries(tags)
|
Object.entries(tags)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|