mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
436 Commits
misc/add-p
...
daniel/gat
Author | SHA1 | Date | |
---|---|---|---|
4c8bf9bd92 | |||
a6554deb80 | |||
4bd1eb6f70 | |||
022ecf75e1 | |||
ce170a6a47 | |||
cb8e36ae15 | |||
16ce1f441e | |||
8043b61c9f | |||
d374ff2093 | |||
eb7c533261 | |||
9a935c9177 | |||
9d24eb15dc | |||
ed4882dfac | |||
7acd7fd522 | |||
2148b636f5 | |||
e40b4a0a4b | |||
311bf8b515 | |||
78c4c3e847 | |||
b8aa36be99 | |||
594445814a | |||
a467b13069 | |||
c425c03939 | |||
9cc17452fa | |||
93ba6f7b58 | |||
0fcb66e9ab | |||
135f425fcf | |||
9c149cb4bf | |||
ce45c1a43d | |||
1a14c71564 | |||
e7fe2ea51e | |||
caa129b565 | |||
30d7e63a67 | |||
a4c21d85ac | |||
c34a139b19 | |||
f2a55da9b6 | |||
a3584d6a8a | |||
36f1559e5e | |||
07902f7db9 | |||
6fddecdf82 | |||
99e2c85f8f | |||
6e1504dc73 | |||
07d930f608 | |||
1101707d8b | |||
696bbcb072 | |||
54435d0ad9 | |||
6c52847dec | |||
698260cba6 | |||
caeda09b21 | |||
1201baf35c | |||
5d5f843a9f | |||
caca23b56c | |||
01ea22f167 | |||
83c53b9d5a | |||
8cc457d49a | |||
540374f543 | |||
4edb90d644 | |||
1a7151aba7 | |||
80d2d9d2cf | |||
4268fdea44 | |||
781965767d | |||
fef7e43869 | |||
9e651a58e3 | |||
0fbf8efd3a | |||
dcb77bbdd4 | |||
36f7e7d81b | |||
8f97b3ad87 | |||
be80444ec2 | |||
6f2043dc26 | |||
6ae7b5e996 | |||
95fcf560a5 | |||
d8ee05bfba | |||
400157a468 | |||
274952544f | |||
d23beaedf1 | |||
73e89fc4db | |||
817e762e6b | |||
ce5712606f | |||
ce67e5f137 | |||
440c45fd42 | |||
893a042c25 | |||
f3fb65fcc3 | |||
c0add863be | |||
5878904f77 | |||
98ab969356 | |||
d4523b0ca4 | |||
2be8c47ae8 | |||
8730d14104 | |||
d924580599 | |||
6e3fe0fe24 | |||
9d11babc4d | |||
ce97179b49 | |||
f9ebb919e5 | |||
739ef8e05a | |||
644659bc10 | |||
21e4fa83ef | |||
a6a6c72397 | |||
4061feba21 | |||
90a415722c | |||
f3d5790e2c | |||
0d0fddb53a | |||
9f2e379d4d | |||
14e898351f | |||
16e0aa13c8 | |||
dc130ecd7f | |||
b70c6b6260 | |||
a701635f08 | |||
9eb98dd276 | |||
96e9bc3b2f | |||
22f32e060b | |||
b4f26aac25 | |||
b634a6c371 | |||
080ae5ce6f | |||
90d213a8ab | |||
52a26b51af | |||
3b28e946cf | |||
dfcf613023 | |||
3ae2ec1f51 | |||
ce4e35e908 | |||
4db82e37c1 | |||
3a8789af76 | |||
79ebfc92e9 | |||
ffca4aa054 | |||
52b3f7e8c8 | |||
9de33d8c23 | |||
97aed61c54 | |||
972dbac7db | |||
5c0e265703 | |||
4efbb8dca6 | |||
09db9e340b | |||
5e3d4edec9 | |||
86348eb434 | |||
d31d28666a | |||
3362ec29cd | |||
3a0e2bf88b | |||
86862b932c | |||
85fefb2a82 | |||
858ec2095e | |||
a5bb80d2cf | |||
3156057278 | |||
b5da1d7a6c | |||
8fa8161602 | |||
b12aca62ff | |||
c9cd843184 | |||
47442b16f5 | |||
0bdb5d3f19 | |||
cd9ab0024e | |||
f4bed26781 | |||
abedb4b53c | |||
29561d37e9 | |||
75e9ea9c5d | |||
d0c10838e1 | |||
4773336a04 | |||
4dc587576b | |||
7097731539 | |||
4261281b0f | |||
ff7ff06a6a | |||
6cbeb4ddf9 | |||
5a07c3d1d4 | |||
d96e880015 | |||
4df6c8c2cc | |||
70860e0d26 | |||
3f3b81f9bf | |||
5181cac9c8 | |||
5af39b1a40 | |||
a9723134f9 | |||
fe237fbf4a | |||
98e79207cc | |||
26375715e4 | |||
5c435f7645 | |||
f7a9e13209 | |||
04908edb5b | |||
e8753a3ce8 | |||
1947989ca5 | |||
c22e616771 | |||
40711ac707 | |||
a47e6910b1 | |||
78c4a591a9 | |||
f6b7717517 | |||
0885620981 | |||
f67511fa19 | |||
e6c97510ca | |||
476671e6ef | |||
b21a5b6425 | |||
66a5691ffd | |||
6bdf62d453 | |||
652a48b520 | |||
3148c54e18 | |||
44367f9149 | |||
286dc39ed2 | |||
bd4cf64fc6 | |||
f4e3d7d576 | |||
8298f9974f | |||
da347e96e1 | |||
5df96234a0 | |||
e78682560c | |||
1602fac5ca | |||
0100bf7032 | |||
e2c49878c6 | |||
e74117b7fd | |||
335aada941 | |||
b949fe06c3 | |||
28e539c481 | |||
5c4c881b60 | |||
8ffb92bfb3 | |||
db9a1726c2 | |||
15986633c7 | |||
c4809bbb54 | |||
6305aab0d1 | |||
456493ff5a | |||
8cfaefcec5 | |||
e39e80a0e7 | |||
8cae92f29e | |||
918911f2e4 | |||
a1aee45eb2 | |||
5fe93dc35a | |||
5e0e7763a3 | |||
f663d1d4a6 | |||
650f6d9585 | |||
90c36eeded | |||
7994034639 | |||
48619ed24c | |||
21fb8df39b | |||
f03a7cc249 | |||
b5c3f17ec1 | |||
99d88f7687 | |||
8e3559828f | |||
93d7c812e7 | |||
f2dcbfa91c | |||
d08510ebe4 | |||
767159bf8f | |||
98457cdb34 | |||
8ed8f1200d | |||
30252c2bcb | |||
9687f33122 | |||
a5282a56c9 | |||
cc3551c417 | |||
9e6fe39609 | |||
2bc91c42a7 | |||
accb21f7ed | |||
8f010e740f | |||
f3768c90c7 | |||
3190ff2eb1 | |||
c7ec825830 | |||
5b7f445e33 | |||
7fe53ab00e | |||
90c17820fc | |||
e739b29b3c | |||
1a89f2a479 | |||
78568bffe2 | |||
1407a122b9 | |||
8168b5faf8 | |||
8b9e035bf6 | |||
d36d0784ca | |||
e69354b546 | |||
64bd5ddcc8 | |||
72088634d8 | |||
f3a84f6001 | |||
13672481a8 | |||
058394f892 | |||
4f26b43789 | |||
4817eb2fc6 | |||
c623c615a1 | |||
034a8112b7 | |||
5fc6fd71ce | |||
f45c917922 | |||
debef510e4 | |||
be37e27dbf | |||
3b62f956e9 | |||
f49e3788cc | |||
1147f87eed | |||
995e3254ba | |||
67d0c53912 | |||
a6fbcb3e01 | |||
db1ca2b89f | |||
f91bbe1f31 | |||
e5f475e8d6 | |||
1e4ca2f48f | |||
8d5e7406c3 | |||
3b230dad9a | |||
782bf2cdc9 | |||
982b506eb8 | |||
e5bc609a2a | |||
b812761bdd | |||
14362dbe6a | |||
b7b90aea33 | |||
14cc21787d | |||
8d147867ed | |||
eb4e727922 | |||
bb276a0dba | |||
7cdb015b81 | |||
ce446fa723 | |||
82f6c9fb58 | |||
6369d13862 | |||
9f91970be2 | |||
c7398d924a | |||
df57364985 | |||
84322f4f68 | |||
f551806737 | |||
5518df116f | |||
73c6c076e8 | |||
ba2a772247 | |||
8fbe46256b | |||
b75bb93d83 | |||
db4db04ba6 | |||
db44d958d3 | |||
12beb06682 | |||
804f8be07d | |||
e81991c545 | |||
28a3bf0b94 | |||
5712c24370 | |||
65bc522ae9 | |||
b950e07ad6 | |||
498bf8244c | |||
4a391c7ac2 | |||
d49c1e4b72 | |||
424e4670e5 | |||
5e803e76d7 | |||
6648397a64 | |||
85edbbcdc3 | |||
a64f8ac776 | |||
b46a0dfc21 | |||
95ef113aea | |||
07bf65b1c3 | |||
12071e4816 | |||
a40d4efa39 | |||
6d509d85f4 | |||
5b200f42a3 | |||
64f724ed95 | |||
b0d5be6221 | |||
2b21c9d348 | |||
f0a45fb7d8 | |||
40398efb06 | |||
a16c1336fc | |||
ef4df9691d | |||
6a23583391 | |||
e8d00161eb | |||
0a5a073db1 | |||
0f14685d54 | |||
d5888d5bbb | |||
8ff95aedd5 | |||
2b948a18f3 | |||
4d173ad163 | |||
7041b88b9d | |||
f06004370d | |||
c1fa344f02 | |||
df75b3b8d3 | |||
e0322c8a7f | |||
e3725dd3ab | |||
dc6a94ccda | |||
e5229a5377 | |||
2e8003ca95 | |||
04989372b1 | |||
77de085ffc | |||
afcae17e91 | |||
c985690e9a | |||
bb2a70b986 | |||
3ac3710273 | |||
92cb034155 | |||
2493bbbc97 | |||
77b42836e7 | |||
949615606f | |||
6cd7657e41 | |||
38bf5e8b1d | |||
a6bafb8adc | |||
e9e1f4ff5d | |||
13afc9c996 | |||
67d4da40ec | |||
ec633c3e3d | |||
1efdb31037 | |||
44aa743d56 | |||
fefb71dd86 | |||
1748052cb0 | |||
c01a98ccf1 | |||
9ea9f90928 | |||
6319f53802 | |||
966294bd0e | |||
e1dee0678e | |||
8b25f202fe | |||
eb31318d39 | |||
7f6dcd3afa | |||
2b4a6ad907 | |||
ba8fcb6891 | |||
c2df8cf869 | |||
e383872486 | |||
490c589a44 | |||
b358f2dbb7 | |||
10ed6f6b52 | |||
e0f1311f6d | |||
1cff92d000 | |||
db8f43385d | |||
41b45c212d | |||
ef9269fe10 | |||
4d95052896 | |||
260679b01d | |||
a77cc77be8 | |||
9bc5c55cd0 | |||
2cbad206b5 | |||
56b7328231 | |||
edefa7698c | |||
60ea4bb579 | |||
04d553f052 | |||
6d10afc9d2 | |||
c2949964b3 | |||
6faad102e2 | |||
8bfd3913da | |||
d1e5ae2d85 | |||
e5555ffd3f | |||
6b95bb0ceb | |||
b0e25a8bd1 | |||
d483e70748 | |||
4b94848a79 | |||
879b12002c | |||
bc93db8603 | |||
c43a87947f | |||
9e1d38a27b | |||
78d5bc823d | |||
e8d424bbb0 | |||
f0c52cc8da | |||
e58dbe853e | |||
f493a617b1 | |||
32a3e1d200 | |||
7447d17e94 | |||
4efa4ad8df | |||
c6e56f0380 | |||
d61216ed62 | |||
580de0565b | |||
bbfd4a44c3 | |||
01e13ca7bd | |||
f5fdd1a266 | |||
bda74ce13e | |||
6a973be6f3 | |||
7f836ed9bc | |||
4d847ab2cb | |||
80cecbb937 | |||
8b6c97d5bc | |||
5641d334cd |
@ -107,6 +107,14 @@ INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||
INF_APP_CONNECTION_GITHUB_APP_ID=
|
||||
|
||||
#github radar app connection
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_ID=
|
||||
INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET=
|
||||
|
||||
#gcp app connection
|
||||
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
||||
|
||||
|
@ -40,3 +40,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:578
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:579
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:581
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:582
|
||||
backend/src/services/smtp/smtp-service.ts:generic-api-key:79
|
||||
|
@ -15,8 +15,8 @@ import { mockSmtpServer } from "./mocks/smtp";
|
||||
import { initDbConnection } from "@app/db";
|
||||
import { queueServiceFactory } from "@app/queue";
|
||||
import { keyStoreFactory } from "@app/keystore/keystore";
|
||||
import { Redis } from "ioredis";
|
||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
||||
export default {
|
||||
@ -30,7 +30,7 @@ export default {
|
||||
dbRootCert: envConfig.DB_ROOT_CERT
|
||||
});
|
||||
|
||||
const redis = new Redis(envConfig.REDIS_URL);
|
||||
const redis = buildRedisFromConfig(envConfig);
|
||||
await redis.flushdb("SYNC");
|
||||
|
||||
try {
|
||||
@ -55,8 +55,8 @@ export default {
|
||||
});
|
||||
|
||||
const smtp = mockSmtpServer();
|
||||
const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI });
|
||||
const keyStore = keyStoreFactory(envConfig.REDIS_URL);
|
||||
const queue = queueServiceFactory(envConfig, { dbConnectionUrl: envConfig.DB_CONNECTION_URI });
|
||||
const keyStore = keyStoreFactory(envConfig);
|
||||
|
||||
const hsmModule = initializeHsmModule(envConfig);
|
||||
hsmModule.initialize();
|
||||
|
1877
backend/package-lock.json
generated
1877
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -131,6 +131,7 @@
|
||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-kms": "^3.609.0",
|
||||
"@aws-sdk/client-route-53": "^3.810.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
"@aws-sdk/client-sts": "^3.600.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
@ -174,6 +175,7 @@
|
||||
"@slack/oauth": "^3.0.2",
|
||||
"@slack/web-api": "^7.8.0",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"acme-client": "^5.4.0",
|
||||
"ajv": "^8.12.0",
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
|
7
backend/src/@types/fastify.d.ts
vendored
7
backend/src/@types/fastify.d.ts
vendored
@ -37,6 +37,7 @@ import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-ap
|
||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||
import { TSecretScanningV2ServiceFactory } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-service";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||
@ -53,6 +54,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||
@ -82,6 +84,7 @@ import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-servi
|
||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { TPkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
@ -110,6 +113,7 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
orgId?: string;
|
||||
identityAuthInfo?: {
|
||||
identityId: string;
|
||||
oidc?: {
|
||||
@ -268,6 +272,9 @@ declare module "fastify" {
|
||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
secretScanningV2: TSecretScanningV2ServiceFactory;
|
||||
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
|
||||
pkiTemplate: TPkiTemplatesServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
73
backend/src/@types/knex.d.ts
vendored
73
backend/src/@types/knex.d.ts
vendored
@ -6,6 +6,9 @@ import {
|
||||
TAccessApprovalPoliciesApprovers,
|
||||
TAccessApprovalPoliciesApproversInsert,
|
||||
TAccessApprovalPoliciesApproversUpdate,
|
||||
TAccessApprovalPoliciesBypassers,
|
||||
TAccessApprovalPoliciesBypassersInsert,
|
||||
TAccessApprovalPoliciesBypassersUpdate,
|
||||
TAccessApprovalPoliciesInsert,
|
||||
TAccessApprovalPoliciesUpdate,
|
||||
TAccessApprovalRequests,
|
||||
@ -68,6 +71,9 @@ import {
|
||||
TDynamicSecrets,
|
||||
TDynamicSecretsInsert,
|
||||
TDynamicSecretsUpdate,
|
||||
TExternalCertificateAuthorities,
|
||||
TExternalCertificateAuthoritiesInsert,
|
||||
TExternalCertificateAuthoritiesUpdate,
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate,
|
||||
@ -155,6 +161,9 @@ import {
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate,
|
||||
TInternalCertificateAuthorities,
|
||||
TInternalCertificateAuthoritiesInsert,
|
||||
TInternalCertificateAuthoritiesUpdate,
|
||||
TInternalKms,
|
||||
TInternalKmsInsert,
|
||||
TInternalKmsUpdate,
|
||||
@ -270,6 +279,9 @@ import {
|
||||
TSecretApprovalPoliciesApprovers,
|
||||
TSecretApprovalPoliciesApproversInsert,
|
||||
TSecretApprovalPoliciesApproversUpdate,
|
||||
TSecretApprovalPoliciesBypassers,
|
||||
TSecretApprovalPoliciesBypassersInsert,
|
||||
TSecretApprovalPoliciesBypassersUpdate,
|
||||
TSecretApprovalPoliciesInsert,
|
||||
TSecretApprovalPoliciesUpdate,
|
||||
TSecretApprovalRequests,
|
||||
@ -324,9 +336,24 @@ import {
|
||||
TSecretRotationV2SecretMappingsInsert,
|
||||
TSecretRotationV2SecretMappingsUpdate,
|
||||
TSecrets,
|
||||
TSecretScanningConfigs,
|
||||
TSecretScanningConfigsInsert,
|
||||
TSecretScanningConfigsUpdate,
|
||||
TSecretScanningDataSources,
|
||||
TSecretScanningDataSourcesInsert,
|
||||
TSecretScanningDataSourcesUpdate,
|
||||
TSecretScanningFindings,
|
||||
TSecretScanningFindingsInsert,
|
||||
TSecretScanningFindingsUpdate,
|
||||
TSecretScanningGitRisks,
|
||||
TSecretScanningGitRisksInsert,
|
||||
TSecretScanningGitRisksUpdate,
|
||||
TSecretScanningResources,
|
||||
TSecretScanningResourcesInsert,
|
||||
TSecretScanningResourcesUpdate,
|
||||
TSecretScanningScans,
|
||||
TSecretScanningScansInsert,
|
||||
TSecretScanningScansUpdate,
|
||||
TSecretSharing,
|
||||
TSecretSharingInsert,
|
||||
TSecretSharingUpdate,
|
||||
@ -538,6 +565,16 @@ declare module "knex/types/tables" {
|
||||
TCertificateAuthorityCrlInsert,
|
||||
TCertificateAuthorityCrlUpdate
|
||||
>;
|
||||
[TableName.InternalCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||
TInternalCertificateAuthorities,
|
||||
TInternalCertificateAuthoritiesInsert,
|
||||
TInternalCertificateAuthoritiesUpdate
|
||||
>;
|
||||
[TableName.ExternalCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||
TExternalCertificateAuthorities,
|
||||
TExternalCertificateAuthoritiesInsert,
|
||||
TExternalCertificateAuthoritiesUpdate
|
||||
>;
|
||||
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||
TCertificateTemplates,
|
||||
@ -804,6 +841,12 @@ declare module "knex/types/tables" {
|
||||
TAccessApprovalPoliciesApproversUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicyBypasser]: KnexOriginal.CompositeTableType<
|
||||
TAccessApprovalPoliciesBypassers,
|
||||
TAccessApprovalPoliciesBypassersInsert,
|
||||
TAccessApprovalPoliciesBypassersUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType<
|
||||
TAccessApprovalRequests,
|
||||
TAccessApprovalRequestsInsert,
|
||||
@ -827,6 +870,11 @@ declare module "knex/types/tables" {
|
||||
TSecretApprovalPoliciesApproversInsert,
|
||||
TSecretApprovalPoliciesApproversUpdate
|
||||
>;
|
||||
[TableName.SecretApprovalPolicyBypasser]: KnexOriginal.CompositeTableType<
|
||||
TSecretApprovalPoliciesBypassers,
|
||||
TSecretApprovalPoliciesBypassersInsert,
|
||||
TSecretApprovalPoliciesBypassersUpdate
|
||||
>;
|
||||
[TableName.SecretApprovalRequest]: KnexOriginal.CompositeTableType<
|
||||
TSecretApprovalRequests,
|
||||
TSecretApprovalRequestsInsert,
|
||||
@ -1074,5 +1122,30 @@ declare module "knex/types/tables" {
|
||||
TGithubOrgSyncConfigsInsert,
|
||||
TGithubOrgSyncConfigsUpdate
|
||||
>;
|
||||
[TableName.SecretScanningDataSource]: KnexOriginal.CompositeTableType<
|
||||
TSecretScanningDataSources,
|
||||
TSecretScanningDataSourcesInsert,
|
||||
TSecretScanningDataSourcesUpdate
|
||||
>;
|
||||
[TableName.SecretScanningResource]: KnexOriginal.CompositeTableType<
|
||||
TSecretScanningResources,
|
||||
TSecretScanningResourcesInsert,
|
||||
TSecretScanningResourcesUpdate
|
||||
>;
|
||||
[TableName.SecretScanningScan]: KnexOriginal.CompositeTableType<
|
||||
TSecretScanningScans,
|
||||
TSecretScanningScansInsert,
|
||||
TSecretScanningScansUpdate
|
||||
>;
|
||||
[TableName.SecretScanningFinding]: KnexOriginal.CompositeTableType<
|
||||
TSecretScanningFindings,
|
||||
TSecretScanningFindingsInsert,
|
||||
TSecretScanningFindingsUpdate
|
||||
>;
|
||||
[TableName.SecretScanningConfig]: KnexOriginal.CompositeTableType<
|
||||
TSecretScanningConfigs,
|
||||
TSecretScanningConfigsInsert,
|
||||
TSecretScanningConfigsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
const hasProjectIdColumn = await knex.schema.hasColumn(TableName.Certificate, "projectId");
|
||||
if (!hasProjectIdColumn) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("projectId", 36).nullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.Certificate}" cert
|
||||
SET "projectId" = ca."projectId"
|
||||
FROM "${TableName.CertificateAuthority}" ca
|
||||
WHERE cert."caId" = ca.id
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("projectId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caId").nullable().alter();
|
||||
t.uuid("caCertId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
if (await knex.schema.hasColumn(TableName.Certificate, "projectId")) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropForeign("projectId");
|
||||
t.dropColumn("projectId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Altering back to notNullable for caId and caCertId will fail
|
||||
}
|
107
backend/src/db/migrations/20250517002225_secret-scanning-v2.ts
Normal file
107
backend/src/db/migrations/20250517002225_secret-scanning-v2.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||
import {
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningScanStatus
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.SecretScanningDataSource))) {
|
||||
await knex.schema.createTable(TableName.SecretScanningDataSource, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("externalId").index(); // if we need a unique way of identifying this data source from an external resource
|
||||
t.string("name", 48).notNullable();
|
||||
t.string("description");
|
||||
t.string("type").notNullable();
|
||||
t.jsonb("config").notNullable();
|
||||
t.binary("encryptedCredentials"); // webhook credentials, etc.
|
||||
t.uuid("connectionId");
|
||||
t.boolean("isAutoScanEnabled").defaultTo(true);
|
||||
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
t.boolean("isDisconnected").notNullable().defaultTo(false);
|
||||
t.unique(["projectId", "name"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretScanningResource))) {
|
||||
await knex.schema.createTable(TableName.SecretScanningResource, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("externalId").notNullable();
|
||||
t.string("name").notNullable();
|
||||
t.string("type").notNullable();
|
||||
t.uuid("dataSourceId").notNullable();
|
||||
t.foreign("dataSourceId").references("id").inTable(TableName.SecretScanningDataSource).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
t.unique(["dataSourceId", "externalId"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SecretScanningResource);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretScanningScan))) {
|
||||
await knex.schema.createTable(TableName.SecretScanningScan, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("status").notNullable().defaultTo(SecretScanningScanStatus.Queued);
|
||||
t.string("statusMessage", 1024);
|
||||
t.string("type").notNullable();
|
||||
t.uuid("resourceId").notNullable();
|
||||
t.foreign("resourceId").references("id").inTable(TableName.SecretScanningResource).onDelete("CASCADE");
|
||||
t.timestamp("createdAt").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretScanningFinding))) {
|
||||
await knex.schema.createTable(TableName.SecretScanningFinding, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("dataSourceName").notNullable();
|
||||
t.string("dataSourceType").notNullable();
|
||||
t.string("resourceName").notNullable();
|
||||
t.string("resourceType").notNullable();
|
||||
t.string("rule").notNullable();
|
||||
t.string("severity").notNullable();
|
||||
t.string("status").notNullable().defaultTo(SecretScanningFindingStatus.Unresolved);
|
||||
t.string("remarks");
|
||||
t.string("fingerprint").notNullable();
|
||||
t.jsonb("details").notNullable();
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("scanId");
|
||||
t.foreign("scanId").references("id").inTable(TableName.SecretScanningScan).onDelete("SET NULL");
|
||||
t.timestamps(true, true, true);
|
||||
t.unique(["projectId", "fingerprint"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SecretScanningFinding);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretScanningConfig))) {
|
||||
await knex.schema.createTable(TableName.SecretScanningConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("projectId").notNullable().unique();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.string("content", 5000);
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SecretScanningConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretScanningFinding);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretScanningFinding);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretScanningScan);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SecretScanningResource);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretScanningResource);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SecretScanningDataSource);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretScanningDataSource);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SecretScanningConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretScanningConfig);
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.SecretSync, (t) => {
|
||||
t.string("name", 64).notNullable().alter();
|
||||
});
|
||||
await knex.schema.alterTable(TableName.ProjectTemplates, (t) => {
|
||||
t.string("name", 64).notNullable().alter();
|
||||
});
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.string("name", 64).notNullable().alter();
|
||||
});
|
||||
await knex.schema.alterTable(TableName.SecretRotationV2, (t) => {
|
||||
t.string("name", 64).notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// No down migration or it will error
|
||||
}
|
205
backend/src/db/migrations/20250521110635_add-external-ca-pki.ts
Normal file
205
backend/src/db/migrations/20250521110635_add-external-ca-pki.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCATable = await knex.schema.hasTable(TableName.CertificateAuthority);
|
||||
const hasExternalCATable = await knex.schema.hasTable(TableName.ExternalCertificateAuthority);
|
||||
const hasInternalCATable = await knex.schema.hasTable(TableName.InternalCertificateAuthority);
|
||||
|
||||
if (hasCATable && !hasInternalCATable) {
|
||||
await knex.schema.createTableLike(TableName.InternalCertificateAuthority, TableName.CertificateAuthority, (t) => {
|
||||
t.uuid("caId").nullable();
|
||||
});
|
||||
|
||||
// @ts-expect-error intentional: migration
|
||||
await knex(TableName.InternalCertificateAuthority).insert(knex(TableName.CertificateAuthority).select("*"));
|
||||
await knex(TableName.InternalCertificateAuthority).update("caId", knex.ref("id"));
|
||||
|
||||
await knex.schema.alterTable(TableName.InternalCertificateAuthority, (t) => {
|
||||
t.dropColumn("projectId");
|
||||
t.dropColumn("requireTemplateForIssuance");
|
||||
t.dropColumn("createdAt");
|
||||
t.dropColumn("updatedAt");
|
||||
t.dropColumn("status");
|
||||
t.uuid("parentCaId")
|
||||
.nullable()
|
||||
.references("id")
|
||||
.inTable(TableName.CertificateAuthority)
|
||||
.onDelete("CASCADE")
|
||||
.alter();
|
||||
t.uuid("activeCaCertId").nullable().references("id").inTable(TableName.CertificateAuthorityCert).alter();
|
||||
t.uuid("caId").notNullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE").alter();
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
|
||||
t.string("name").nullable();
|
||||
});
|
||||
|
||||
// prefill name for existing internal CAs and flip enableDirectIssuance
|
||||
const cas = await knex(TableName.CertificateAuthority).select("id", "friendlyName", "enableDirectIssuance");
|
||||
await Promise.all(
|
||||
cas.map((ca) => {
|
||||
const slugifiedName = ca.friendlyName
|
||||
? slugify(`${ca.friendlyName.slice(0, 16)}-${alphaNumericNanoId(8)}`)
|
||||
: slugify(alphaNumericNanoId(12));
|
||||
|
||||
return knex(TableName.CertificateAuthority)
|
||||
.where({ id: ca.id })
|
||||
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
|
||||
})
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.dropColumn("parentCaId");
|
||||
t.dropColumn("type");
|
||||
t.dropColumn("friendlyName");
|
||||
t.dropColumn("organization");
|
||||
t.dropColumn("ou");
|
||||
t.dropColumn("country");
|
||||
t.dropColumn("province");
|
||||
t.dropColumn("locality");
|
||||
t.dropColumn("commonName");
|
||||
t.dropColumn("dn");
|
||||
t.dropColumn("serialNumber");
|
||||
t.dropColumn("maxPathLength");
|
||||
t.dropColumn("keyAlgorithm");
|
||||
t.dropColumn("notBefore");
|
||||
t.dropColumn("notAfter");
|
||||
t.dropColumn("activeCaCertId");
|
||||
t.boolean("enableDirectIssuance").notNullable().defaultTo(true).alter();
|
||||
t.string("name").notNullable().alter();
|
||||
t.unique(["name", "projectId"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasExternalCATable) {
|
||||
await knex.schema.createTable(TableName.ExternalCertificateAuthority, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("type").notNullable();
|
||||
t.uuid("appConnectionId").nullable();
|
||||
t.foreign("appConnectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.uuid("dnsAppConnectionId").nullable();
|
||||
t.foreign("dnsAppConnectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.uuid("caId").notNullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||
t.binary("credentials");
|
||||
t.json("configuration");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.string("ttl").nullable().alter();
|
||||
|
||||
t.boolean("enableAutoRenewal").notNullable().defaultTo(false);
|
||||
t.integer("autoRenewalPeriodInDays");
|
||||
t.datetime("lastAutoRenewAt");
|
||||
|
||||
t.string("lastOperationStatus");
|
||||
t.text("lastOperationMessage");
|
||||
t.dateTime("lastOperationAt");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCATable = await knex.schema.hasTable(TableName.CertificateAuthority);
|
||||
const hasExternalCATable = await knex.schema.hasTable(TableName.ExternalCertificateAuthority);
|
||||
const hasInternalCATable = await knex.schema.hasTable(TableName.InternalCertificateAuthority);
|
||||
|
||||
if (hasCATable && hasInternalCATable) {
|
||||
// First add all columns as nullable
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.uuid("parentCaId").nullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||
t.string("type").nullable();
|
||||
t.string("friendlyName").nullable();
|
||||
t.string("organization").nullable();
|
||||
t.string("ou").nullable();
|
||||
t.string("country").nullable();
|
||||
t.string("province").nullable();
|
||||
t.string("locality").nullable();
|
||||
t.string("commonName").nullable();
|
||||
t.string("dn").nullable();
|
||||
t.string("serialNumber").nullable().unique();
|
||||
t.integer("maxPathLength").nullable();
|
||||
t.string("keyAlgorithm").nullable();
|
||||
t.timestamp("notBefore").nullable();
|
||||
t.timestamp("notAfter").nullable();
|
||||
t.uuid("activeCaCertId").nullable().references("id").inTable(TableName.CertificateAuthorityCert);
|
||||
t.renameColumn("enableDirectIssuance", "requireTemplateForIssuance");
|
||||
t.dropColumn("name");
|
||||
});
|
||||
|
||||
// flip requireTemplateForIssuance for existing internal CAs
|
||||
const cas = await knex(TableName.CertificateAuthority).select("id", "requireTemplateForIssuance");
|
||||
await Promise.all(
|
||||
cas.map((ca) => {
|
||||
return (
|
||||
knex(TableName.CertificateAuthority)
|
||||
.where({ id: ca.id })
|
||||
// @ts-expect-error intentional: migration
|
||||
.update({ requireTemplateForIssuance: !ca.requireTemplateForIssuance })
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE ${TableName.CertificateAuthority} ca
|
||||
SET
|
||||
type = ica.type,
|
||||
"friendlyName" = ica."friendlyName",
|
||||
organization = ica.organization,
|
||||
ou = ica.ou,
|
||||
country = ica.country,
|
||||
province = ica.province,
|
||||
locality = ica.locality,
|
||||
"commonName" = ica."commonName",
|
||||
dn = ica.dn,
|
||||
"parentCaId" = ica."parentCaId",
|
||||
"serialNumber" = ica."serialNumber",
|
||||
"maxPathLength" = ica."maxPathLength",
|
||||
"keyAlgorithm" = ica."keyAlgorithm",
|
||||
"notBefore" = ica."notBefore",
|
||||
"notAfter" = ica."notAfter",
|
||||
"activeCaCertId" = ica."activeCaCertId"
|
||||
FROM ${TableName.InternalCertificateAuthority} ica
|
||||
WHERE ca.id = ica."caId"
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.string("type").notNullable().alter();
|
||||
t.string("friendlyName").notNullable().alter();
|
||||
t.string("organization").notNullable().alter();
|
||||
t.string("ou").notNullable().alter();
|
||||
t.string("country").notNullable().alter();
|
||||
t.string("province").notNullable().alter();
|
||||
t.string("locality").notNullable().alter();
|
||||
t.string("commonName").notNullable().alter();
|
||||
t.string("dn").notNullable().alter();
|
||||
t.string("keyAlgorithm").notNullable().alter();
|
||||
t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false).alter();
|
||||
});
|
||||
|
||||
await knex.schema.dropTable(TableName.InternalCertificateAuthority);
|
||||
}
|
||||
|
||||
if (hasExternalCATable) {
|
||||
await knex.schema.dropTable(TableName.ExternalCertificateAuthority);
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.dropColumn("enableAutoRenewal");
|
||||
t.dropColumn("autoRenewalPeriodInDays");
|
||||
t.dropColumn("lastAutoRenewAt");
|
||||
|
||||
t.dropColumn("lastOperationStatus");
|
||||
t.dropColumn("lastOperationMessage");
|
||||
t.dropColumn("lastOperationAt");
|
||||
});
|
||||
}
|
||||
}
|
48
backend/src/db/migrations/20250527030702_policy-bypassers.ts
Normal file
48
backend/src/db/migrations/20250527030702_policy-bypassers.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyBypasser))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicyBypasser, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("bypasserGroupId").nullable();
|
||||
t.foreign("bypasserGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
|
||||
t.uuid("bypasserUserId").nullable();
|
||||
t.foreign("bypasserUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyBypasser);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretApprovalPolicyBypasser))) {
|
||||
await knex.schema.createTable(TableName.SecretApprovalPolicyBypasser, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("bypasserGroupId").nullable();
|
||||
t.foreign("bypasserGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
|
||||
t.uuid("bypasserUserId").nullable();
|
||||
t.foreign("bypasserUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.SecretApprovalPolicy).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyBypasser);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretApprovalPolicyBypasser);
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyBypasser);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretApprovalPolicyBypasser);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyBypasser);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "usernameTemplate");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
t.string("usernameTemplate").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "usernameTemplate");
|
||||
if (hasColumn) {
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
t.dropColumn("usernameTemplate");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityAccessToken, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityAwsAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityOidcAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityAzureAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityAzureAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityGcpAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityJwtAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityJwtAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityOciAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityOciAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityTokenAuth, "accessTokenPeriod"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityTokenAuth, (t) => {
|
||||
t.bigInteger("accessTokenPeriod").defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.IdentityAccessToken, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityAwsAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityOidcAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityAzureAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityAzureAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityGcpAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityJwtAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityJwtAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityOciAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityOciAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.IdentityTokenAuth, "accessTokenPeriod")) {
|
||||
await knex.schema.alterTable(TableName.IdentityTokenAuth, (t) => {
|
||||
t.dropColumn("accessTokenPeriod");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasNameCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "name");
|
||||
if (hasNameCol) {
|
||||
const templates = await knex(TableName.CertificateTemplate).select("id", "name");
|
||||
await Promise.all(
|
||||
templates.map((el) => {
|
||||
const slugifiedName = el.name
|
||||
? slugify(`${el.name.slice(0, 16)}-${alphaNumericNanoId(8)}`)
|
||||
: slugify(alphaNumericNanoId(12));
|
||||
|
||||
return knex(TableName.CertificateTemplate).where({ id: el.id }).update({ name: slugifiedName });
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
@ -0,0 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
|
||||
|
||||
if (hasEncryptedSalt) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("encryptedSalt");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
|
||||
|
||||
if (!hasEncryptedSalt) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.binary("encryptedSalt").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ApprovalStatus } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasPrivilegeDeletedAtColumn = await knex.schema.hasColumn(
|
||||
TableName.AccessApprovalRequest,
|
||||
"privilegeDeletedAt"
|
||||
);
|
||||
const hasStatusColumn = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "status");
|
||||
|
||||
if (!hasPrivilegeDeletedAtColumn) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.timestamp("privilegeDeletedAt").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasStatusColumn) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.string("status").defaultTo(ApprovalStatus.PENDING).notNullable();
|
||||
});
|
||||
|
||||
// Update existing rows based on business logic
|
||||
// If privilegeId is not null, set status to "approved"
|
||||
await knex(TableName.AccessApprovalRequest).whereNotNull("privilegeId").update({ status: ApprovalStatus.APPROVED });
|
||||
|
||||
// If privilegeId is null and there's a rejected reviewer, set to "rejected"
|
||||
const rejectedRequestIds = await knex(TableName.AccessApprovalRequestReviewer)
|
||||
.select("requestId")
|
||||
.where("status", "rejected")
|
||||
.distinct()
|
||||
.pluck("requestId");
|
||||
|
||||
if (rejectedRequestIds.length > 0) {
|
||||
await knex(TableName.AccessApprovalRequest)
|
||||
.whereNull("privilegeId")
|
||||
.whereIn("id", rejectedRequestIds)
|
||||
.update({ status: ApprovalStatus.REJECTED });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasPrivilegeDeletedAtColumn = await knex.schema.hasColumn(
|
||||
TableName.AccessApprovalRequest,
|
||||
"privilegeDeletedAt"
|
||||
);
|
||||
const hasStatusColumn = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "status");
|
||||
|
||||
if (hasPrivilegeDeletedAtColumn) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.dropColumn("privilegeDeletedAt");
|
||||
});
|
||||
}
|
||||
|
||||
if (hasStatusColumn) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.dropColumn("status");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode");
|
||||
|
||||
if (!hasTokenReviewModeColumn) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.string("tokenReviewMode").notNullable().defaultTo("api");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode");
|
||||
|
||||
if (hasTokenReviewModeColumn) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.dropColumn("tokenReviewMode");
|
||||
});
|
||||
}
|
||||
}
|
26
backend/src/db/schemas/access-approval-policies-bypassers.ts
Normal file
26
backend/src/db/schemas/access-approval-policies-bypassers.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 AccessApprovalPoliciesBypassersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
bypasserGroupId: z.string().uuid().nullable().optional(),
|
||||
bypasserUserId: z.string().uuid().nullable().optional(),
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPoliciesBypassers = z.infer<typeof AccessApprovalPoliciesBypassersSchema>;
|
||||
export type TAccessApprovalPoliciesBypassersInsert = Omit<
|
||||
z.input<typeof AccessApprovalPoliciesBypassersSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalPoliciesBypassersUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesBypassersSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -18,7 +18,9 @@ export const AccessApprovalRequestsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
requestedByUserId: z.string().uuid(),
|
||||
note: z.string().nullable().optional()
|
||||
note: z.string().nullable().optional(),
|
||||
privilegeDeletedAt: z.date().nullable().optional(),
|
||||
status: z.string().default("pending")
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||
|
@ -11,25 +11,10 @@ export const CertificateAuthoritiesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
parentCaId: z.string().uuid().nullable().optional(),
|
||||
projectId: z.string(),
|
||||
type: z.string(),
|
||||
enableDirectIssuance: z.boolean().default(true),
|
||||
status: z.string(),
|
||||
friendlyName: z.string(),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
dn: z.string(),
|
||||
serialNumber: z.string().nullable().optional(),
|
||||
maxPathLength: z.number().nullable().optional(),
|
||||
keyAlgorithm: z.string(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional(),
|
||||
activeCaCertId: z.string().uuid().nullable().optional(),
|
||||
requireTemplateForIssuance: z.boolean().default(false)
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||
|
@ -11,7 +11,7 @@ export const CertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
caId: z.string().uuid(),
|
||||
caId: z.string().uuid().nullable().optional(),
|
||||
status: z.string(),
|
||||
serialNumber: z.string(),
|
||||
friendlyName: z.string(),
|
||||
@ -21,11 +21,12 @@ export const CertificatesSchema = z.object({
|
||||
revokedAt: z.date().nullable().optional(),
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().nullable().optional(),
|
||||
caCertId: z.string().uuid(),
|
||||
caCertId: z.string().uuid().nullable().optional(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||
pkiSubscriberId: z.string().uuid().nullable().optional()
|
||||
pkiSubscriberId: z.string().uuid().nullable().optional(),
|
||||
projectId: z.string()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
|
@ -28,7 +28,8 @@ export const DynamicSecretsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
encryptedInput: zodBuffer,
|
||||
projectGatewayId: z.string().uuid().nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional()
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
usernameTemplate: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
|
29
backend/src/db/schemas/external-certificate-authorities.ts
Normal file
29
backend/src/db/schemas/external-certificate-authorities.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ExternalCertificateAuthoritiesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: z.string(),
|
||||
appConnectionId: z.string().uuid().nullable().optional(),
|
||||
dnsAppConnectionId: z.string().uuid().nullable().optional(),
|
||||
caId: z.string().uuid(),
|
||||
credentials: zodBuffer.nullable().optional(),
|
||||
configuration: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TExternalCertificateAuthorities = z.infer<typeof ExternalCertificateAuthoritiesSchema>;
|
||||
export type TExternalCertificateAuthoritiesInsert = Omit<
|
||||
z.input<typeof ExternalCertificateAuthoritiesSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TExternalCertificateAuthoritiesUpdate = Partial<
|
||||
Omit<z.input<typeof ExternalCertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -21,7 +21,8 @@ export const IdentityAccessTokensSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string().nullable().optional(),
|
||||
authMethod: z.string()
|
||||
authMethod: z.string(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;
|
||||
|
@ -19,7 +19,8 @@ export const IdentityAwsAuthsSchema = z.object({
|
||||
type: z.string(),
|
||||
stsEndpoint: z.string(),
|
||||
allowedPrincipalArns: z.string(),
|
||||
allowedAccountIds: z.string()
|
||||
allowedAccountIds: z.string(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityAwsAuths = z.infer<typeof IdentityAwsAuthsSchema>;
|
||||
|
@ -18,7 +18,8 @@ export const IdentityAzureAuthsSchema = z.object({
|
||||
identityId: z.string().uuid(),
|
||||
tenantId: z.string(),
|
||||
resource: z.string(),
|
||||
allowedServicePrincipalIds: z.string()
|
||||
allowedServicePrincipalIds: z.string(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityAzureAuths = z.infer<typeof IdentityAzureAuthsSchema>;
|
||||
|
@ -19,7 +19,8 @@ export const IdentityGcpAuthsSchema = z.object({
|
||||
type: z.string(),
|
||||
allowedServiceAccounts: z.string().nullable().optional(),
|
||||
allowedProjects: z.string().nullable().optional(),
|
||||
allowedZones: z.string().nullable().optional()
|
||||
allowedZones: z.string().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
|
||||
|
@ -25,7 +25,8 @@ export const IdentityJwtAuthsSchema = z.object({
|
||||
boundClaims: z.unknown(),
|
||||
boundSubject: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityJwtAuths = z.infer<typeof IdentityJwtAuthsSchema>;
|
||||
|
@ -30,7 +30,9 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
allowedAudience: z.string(),
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional()
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0),
|
||||
tokenReviewMode: z.string().default("api")
|
||||
});
|
||||
|
||||
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
|
||||
|
@ -24,7 +24,8 @@ export const IdentityLdapAuthsSchema = z.object({
|
||||
searchFilter: z.string(),
|
||||
allowedFields: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
||||
|
@ -18,7 +18,8 @@ export const IdentityOciAuthsSchema = z.object({
|
||||
identityId: z.string().uuid(),
|
||||
type: z.string(),
|
||||
tenancyOcid: z.string(),
|
||||
allowedUsernames: z.string().nullable().optional()
|
||||
allowedUsernames: z.string().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityOciAuths = z.infer<typeof IdentityOciAuthsSchema>;
|
||||
|
@ -27,7 +27,8 @@ export const IdentityOidcAuthsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional(),
|
||||
claimMetadataMapping: z.unknown().nullable().optional()
|
||||
claimMetadataMapping: z.unknown().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
||||
|
@ -15,7 +15,8 @@ export const IdentityTokenAuthsSchema = z.object({
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid()
|
||||
identityId: z.string().uuid(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityTokenAuths = z.infer<typeof IdentityTokenAuthsSchema>;
|
||||
|
@ -17,7 +17,8 @@ export const IdentityUniversalAuthsSchema = z.object({
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid()
|
||||
identityId: z.string().uuid(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
});
|
||||
|
||||
export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>;
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from "./access-approval-policies";
|
||||
export * from "./access-approval-policies-approvers";
|
||||
export * from "./access-approval-policies-bypassers";
|
||||
export * from "./access-approval-requests";
|
||||
export * from "./access-approval-requests-reviewers";
|
||||
export * from "./api-keys";
|
||||
@ -20,6 +21,7 @@ export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-certificate-authorities";
|
||||
export * from "./external-group-org-role-mappings";
|
||||
export * from "./external-kms";
|
||||
export * from "./gateways";
|
||||
@ -49,6 +51,7 @@ export * from "./identity-universal-auths";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./internal-certificate-authorities";
|
||||
export * from "./internal-kms";
|
||||
export * from "./kmip-client-certificates";
|
||||
export * from "./kmip-clients";
|
||||
@ -90,6 +93,7 @@ export * from "./saml-configs";
|
||||
export * from "./scim-tokens";
|
||||
export * from "./secret-approval-policies";
|
||||
export * from "./secret-approval-policies-approvers";
|
||||
export * from "./secret-approval-policies-bypassers";
|
||||
export * from "./secret-approval-request-secret-tags";
|
||||
export * from "./secret-approval-request-secret-tags-v2";
|
||||
export * from "./secret-approval-requests";
|
||||
@ -107,7 +111,12 @@ export * from "./secret-rotation-outputs";
|
||||
export * from "./secret-rotation-v2-secret-mappings";
|
||||
export * from "./secret-rotations";
|
||||
export * from "./secret-rotations-v2";
|
||||
export * from "./secret-scanning-configs";
|
||||
export * from "./secret-scanning-data-sources";
|
||||
export * from "./secret-scanning-findings";
|
||||
export * from "./secret-scanning-git-risks";
|
||||
export * from "./secret-scanning-resources";
|
||||
export * from "./secret-scanning-scans";
|
||||
export * from "./secret-sharing";
|
||||
export * from "./secret-snapshot-folders";
|
||||
export * from "./secret-snapshot-secrets";
|
||||
|
38
backend/src/db/schemas/internal-certificate-authorities.ts
Normal file
38
backend/src/db/schemas/internal-certificate-authorities.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 InternalCertificateAuthoritiesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
parentCaId: z.string().uuid().nullable().optional(),
|
||||
type: z.string(),
|
||||
friendlyName: z.string(),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
dn: z.string(),
|
||||
serialNumber: z.string().nullable().optional(),
|
||||
maxPathLength: z.number().nullable().optional(),
|
||||
keyAlgorithm: z.string(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional(),
|
||||
activeCaCertId: z.string().uuid().nullable().optional(),
|
||||
caId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TInternalCertificateAuthorities = z.infer<typeof InternalCertificateAuthoritiesSchema>;
|
||||
export type TInternalCertificateAuthoritiesInsert = Omit<
|
||||
z.input<typeof InternalCertificateAuthoritiesSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TInternalCertificateAuthoritiesUpdate = Partial<
|
||||
Omit<z.input<typeof InternalCertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -13,6 +13,8 @@ export enum TableName {
|
||||
SshCertificate = "ssh_certificates",
|
||||
SshCertificateBody = "ssh_certificate_bodies",
|
||||
CertificateAuthority = "certificate_authorities",
|
||||
ExternalCertificateAuthority = "external_certificate_authorities",
|
||||
InternalCertificateAuthority = "internal_certificate_authorities",
|
||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||
CertificateAuthorityCert = "certificate_authority_certs",
|
||||
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||
@ -93,10 +95,12 @@ export enum TableName {
|
||||
ScimToken = "scim_tokens",
|
||||
AccessApprovalPolicy = "access_approval_policies",
|
||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||
AccessApprovalPolicyBypasser = "access_approval_policies_bypassers",
|
||||
AccessApprovalRequest = "access_approval_requests",
|
||||
AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
|
||||
SecretApprovalPolicy = "secret_approval_policies",
|
||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||
SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers",
|
||||
SecretApprovalRequest = "secret_approval_requests",
|
||||
SecretApprovalRequestReviewer = "secret_approval_requests_reviewers",
|
||||
SecretApprovalRequestSecret = "secret_approval_requests_secrets",
|
||||
@ -155,7 +159,12 @@ export enum TableName {
|
||||
MicrosoftTeamsIntegrations = "microsoft_teams_integrations",
|
||||
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
|
||||
SecretReminderRecipients = "secret_reminder_recipients",
|
||||
GithubOrgSyncConfig = "github_org_sync_configs"
|
||||
GithubOrgSyncConfig = "github_org_sync_configs",
|
||||
SecretScanningDataSource = "secret_scanning_data_sources",
|
||||
SecretScanningResource = "secret_scanning_resources",
|
||||
SecretScanningScan = "secret_scanning_scans",
|
||||
SecretScanningFinding = "secret_scanning_findings",
|
||||
SecretScanningConfig = "secret_scanning_configs"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
@ -244,7 +253,8 @@ export enum ProjectType {
|
||||
SecretManager = "secret-manager",
|
||||
CertificateManager = "cert-manager",
|
||||
KMS = "kms",
|
||||
SSH = "ssh"
|
||||
SSH = "ssh",
|
||||
SecretScanning = "secret-scanning"
|
||||
}
|
||||
|
||||
export enum ActionProjectType {
|
||||
@ -252,6 +262,7 @@ export enum ActionProjectType {
|
||||
CertificateManager = ProjectType.CertificateManager,
|
||||
KMS = ProjectType.KMS,
|
||||
SSH = ProjectType.SSH,
|
||||
SecretScanning = ProjectType.SecretScanning,
|
||||
// project operations that happen on all types
|
||||
Any = "any"
|
||||
}
|
||||
|
@ -16,10 +16,16 @@ export const PkiSubscribersSchema = z.object({
|
||||
name: z.string(),
|
||||
commonName: z.string(),
|
||||
subjectAlternativeNames: z.string().array(),
|
||||
ttl: z.string(),
|
||||
ttl: z.string().nullable().optional(),
|
||||
keyUsages: z.string().array(),
|
||||
extendedKeyUsages: z.string().array(),
|
||||
status: z.string()
|
||||
status: z.string(),
|
||||
enableAutoRenewal: z.boolean().default(false),
|
||||
autoRenewalPeriodInDays: z.number().nullable().optional(),
|
||||
lastAutoRenewAt: z.date().nullable().optional(),
|
||||
lastOperationStatus: z.string().nullable().optional(),
|
||||
lastOperationMessage: z.string().nullable().optional(),
|
||||
lastOperationAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
|
||||
|
26
backend/src/db/schemas/secret-approval-policies-bypassers.ts
Normal file
26
backend/src/db/schemas/secret-approval-policies-bypassers.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 SecretApprovalPoliciesBypassersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
bypasserGroupId: z.string().uuid().nullable().optional(),
|
||||
bypasserUserId: z.string().uuid().nullable().optional(),
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPoliciesBypassers = z.infer<typeof SecretApprovalPoliciesBypassersSchema>;
|
||||
export type TSecretApprovalPoliciesBypassersInsert = Omit<
|
||||
z.input<typeof SecretApprovalPoliciesBypassersSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TSecretApprovalPoliciesBypassersUpdate = Partial<
|
||||
Omit<z.input<typeof SecretApprovalPoliciesBypassersSchema>, TImmutableDBKeys>
|
||||
>;
|
20
backend/src/db/schemas/secret-scanning-configs.ts
Normal file
20
backend/src/db/schemas/secret-scanning-configs.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// 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 SecretScanningConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
content: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretScanningConfigs = z.infer<typeof SecretScanningConfigsSchema>;
|
||||
export type TSecretScanningConfigsInsert = Omit<z.input<typeof SecretScanningConfigsSchema>, TImmutableDBKeys>;
|
||||
export type TSecretScanningConfigsUpdate = Partial<Omit<z.input<typeof SecretScanningConfigsSchema>, TImmutableDBKeys>>;
|
32
backend/src/db/schemas/secret-scanning-data-sources.ts
Normal file
32
backend/src/db/schemas/secret-scanning-data-sources.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretScanningDataSourcesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
externalId: z.string().nullable().optional(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
config: z.unknown(),
|
||||
encryptedCredentials: zodBuffer.nullable().optional(),
|
||||
connectionId: z.string().uuid().nullable().optional(),
|
||||
isAutoScanEnabled: z.boolean().default(true).nullable().optional(),
|
||||
projectId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
isDisconnected: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TSecretScanningDataSources = z.infer<typeof SecretScanningDataSourcesSchema>;
|
||||
export type TSecretScanningDataSourcesInsert = Omit<z.input<typeof SecretScanningDataSourcesSchema>, TImmutableDBKeys>;
|
||||
export type TSecretScanningDataSourcesUpdate = Partial<
|
||||
Omit<z.input<typeof SecretScanningDataSourcesSchema>, TImmutableDBKeys>
|
||||
>;
|
32
backend/src/db/schemas/secret-scanning-findings.ts
Normal file
32
backend/src/db/schemas/secret-scanning-findings.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// 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 SecretScanningFindingsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
dataSourceName: z.string(),
|
||||
dataSourceType: z.string(),
|
||||
resourceName: z.string(),
|
||||
resourceType: z.string(),
|
||||
rule: z.string(),
|
||||
severity: z.string(),
|
||||
status: z.string().default("unresolved"),
|
||||
remarks: z.string().nullable().optional(),
|
||||
fingerprint: z.string(),
|
||||
details: z.unknown(),
|
||||
projectId: z.string(),
|
||||
scanId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretScanningFindings = z.infer<typeof SecretScanningFindingsSchema>;
|
||||
export type TSecretScanningFindingsInsert = Omit<z.input<typeof SecretScanningFindingsSchema>, TImmutableDBKeys>;
|
||||
export type TSecretScanningFindingsUpdate = Partial<
|
||||
Omit<z.input<typeof SecretScanningFindingsSchema>, TImmutableDBKeys>
|
||||
>;
|
24
backend/src/db/schemas/secret-scanning-resources.ts
Normal file
24
backend/src/db/schemas/secret-scanning-resources.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretScanningResourcesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
externalId: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
dataSourceId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretScanningResources = z.infer<typeof SecretScanningResourcesSchema>;
|
||||
export type TSecretScanningResourcesInsert = Omit<z.input<typeof SecretScanningResourcesSchema>, TImmutableDBKeys>;
|
||||
export type TSecretScanningResourcesUpdate = Partial<
|
||||
Omit<z.input<typeof SecretScanningResourcesSchema>, TImmutableDBKeys>
|
||||
>;
|
21
backend/src/db/schemas/secret-scanning-scans.ts
Normal file
21
backend/src/db/schemas/secret-scanning-scans.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretScanningScansSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
status: z.string().default("queued"),
|
||||
statusMessage: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
resourceId: z.string().uuid(),
|
||||
createdAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretScanningScans = z.infer<typeof SecretScanningScansSchema>;
|
||||
export type TSecretScanningScansInsert = Omit<z.input<typeof SecretScanningScansSchema>, TImmutableDBKeys>;
|
||||
export type TSecretScanningScansUpdate = Partial<Omit<z.input<typeof SecretScanningScansSchema>, TImmutableDBKeys>>;
|
@ -28,7 +28,6 @@ export const SecretSharingSchema = z.object({
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional(),
|
||||
type: z.string().default("share"),
|
||||
encryptedSalt: zodBuffer.nullable().optional(),
|
||||
authorizedEmails: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -24,10 +24,19 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.max(100, "Cannot have more than 100 approvers")
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
bypassers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.max(100, "Cannot have more than 100 bypassers")
|
||||
.optional(),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
@ -72,7 +81,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
.optional(),
|
||||
bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array()
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
@ -143,10 +153,19 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
.min(1, { message: "At least one approver should be provided" })
|
||||
.max(100, "Cannot have more than 100 approvers"),
|
||||
bypassers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.max(100, "Cannot have more than 100 bypassers")
|
||||
.optional(),
|
||||
approvals: z.number().min(1).optional(),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
@ -220,6 +239,15 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
.optional(),
|
||||
bypassers: z
|
||||
.object({
|
||||
type: z.nativeEnum(BypasserType),
|
||||
id: z.string().nullable().optional(),
|
||||
name: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
|
@ -113,6 +113,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
bypassers: z.string().array(),
|
||||
secretPath: z.string().nullish(),
|
||||
envId: z.string(),
|
||||
enforcementLevel: z.string(),
|
||||
@ -154,7 +155,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
requestId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]),
|
||||
bypassReason: z.string().min(10).max(1000).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -170,7 +172,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
requestId: req.params.requestId,
|
||||
status: req.body.status
|
||||
status: req.body.status,
|
||||
bypassReason: req.body.bypassReason
|
||||
});
|
||||
|
||||
return { review };
|
||||
|
@ -1,16 +1,16 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateOCIConnectionSchema,
|
||||
SanitizedOCIConnectionSchema,
|
||||
UpdateOCIConnectionSchema
|
||||
} from "@app/services/app-connection/oci";
|
||||
} from "@app/ee/services/app-connections/oci";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
import { registerAppConnectionEndpoints } from "../../../../server/routes/v1/app-connection-routers/app-connection-endpoints";
|
||||
|
||||
export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
@ -6,6 +6,8 @@ import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -13,6 +15,28 @@ import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchema
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
|
||||
const validateUsernameTemplateCharacters = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.OpenBrace,
|
||||
CharacterType.CloseBrace,
|
||||
CharacterType.CloseBracket,
|
||||
CharacterType.OpenBracket,
|
||||
CharacterType.Fullstop
|
||||
]);
|
||||
|
||||
const userTemplateSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.refine((el) => validateUsernameTemplateCharacters(el))
|
||||
.refine((el) =>
|
||||
isValidHandleBarTemplate(el, {
|
||||
allowedExpressions: (val) => ["randomUsername", "unixTimestamp"].includes(val)
|
||||
})
|
||||
);
|
||||
|
||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@ -52,7 +76,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
metadata: ResourceMetadataSchema.optional(),
|
||||
usernameTemplate: userTemplateSchema.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -73,39 +98,6 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/entra-id/users",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
|
||||
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
|
||||
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string().min(1).describe("The name of the user"),
|
||||
id: z.string().min(1).describe("The ID of the user"),
|
||||
email: z.string().min(1).describe("The email of the user")
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
|
||||
tenantId: req.body.tenantId,
|
||||
applicationId: req.body.applicationId,
|
||||
clientSecret: req.body.clientSecret
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:name",
|
||||
@ -150,7 +142,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
})
|
||||
.nullable(),
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
metadata: ResourceMetadataSchema.optional(),
|
||||
usernameTemplate: userTemplateSchema.nullable().optional()
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
@ -328,4 +321,37 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
return { leases };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/entra-id/users",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
|
||||
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
|
||||
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string().min(1).describe("The name of the user"),
|
||||
id: z.string().min(1).describe("The ID of the user"),
|
||||
email: z.string().min(1).describe("The email of the user")
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
|
||||
tenantId: req.body.tenantId,
|
||||
applicationId: req.body.applicationId,
|
||||
clientSecret: req.body.clientSecret
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -47,7 +47,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
200: z.object({ plan: z.any() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const plan = await server.services.license.getOrgPlan({
|
||||
actorId: req.permission.id,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -30,10 +30,19 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
.min(1, { message: "At least one approver should be provided" })
|
||||
.max(100, "Cannot have more than 100 approvers"),
|
||||
bypassers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.max(100, "Cannot have more than 100 bypassers")
|
||||
.optional(),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
@ -75,10 +84,19 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
approvers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() })
|
||||
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.min(1, { message: "At least one approver should be provided" }),
|
||||
.min(1, { message: "At least one approver should be provided" })
|
||||
.max(100, "Cannot have more than 100 approvers"),
|
||||
bypassers: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
|
||||
z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
|
||||
])
|
||||
.array()
|
||||
.max(100, "Cannot have more than 100 bypassers")
|
||||
.optional(),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z
|
||||
.string()
|
||||
@ -157,6 +175,12 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType)
|
||||
})
|
||||
.array(),
|
||||
bypassers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(BypasserType)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
@ -193,7 +217,14 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(ApproverType),
|
||||
name: z.string().nullable().optional()
|
||||
username: z.string().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
bypassers: z
|
||||
.object({
|
||||
id: z.string().nullable().optional(),
|
||||
type: z.nativeEnum(BypasserType),
|
||||
username: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
|
@ -47,6 +47,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
userId: z.string().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
bypassers: z
|
||||
.object({
|
||||
userId: z.string().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
@ -266,6 +271,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: approvalRequestUser.array(),
|
||||
bypassers: approvalRequestUser.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
|
@ -2,11 +2,10 @@ import {
|
||||
CreateOCIVaultSyncSchema,
|
||||
OCIVaultSyncSchema,
|
||||
UpdateOCIVaultSyncSchema
|
||||
} from "@app/services/secret-sync/oci-vault";
|
||||
} from "@app/ee/services/secret-sync/oci-vault";
|
||||
import { registerSyncSecretsEndpoints } from "@app/server/routes/v1/secret-sync-routers/secret-sync-endpoints";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.OCIVault,
|
@ -2,6 +2,10 @@ import {
|
||||
registerSecretRotationV2Router,
|
||||
SECRET_ROTATION_REGISTER_ROUTER_MAP
|
||||
} from "@app/ee/routes/v2/secret-rotation-v2-routers";
|
||||
import {
|
||||
registerSecretScanningV2Router,
|
||||
SECRET_SCANNING_REGISTER_ROUTER_MAP
|
||||
} from "@app/ee/routes/v2/secret-scanning-v2-routers";
|
||||
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
@ -31,4 +35,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ prefix: "/secret-rotations" }
|
||||
);
|
||||
|
||||
await server.register(
|
||||
async (secretScanningV2Router) => {
|
||||
// register generic secret scanning endpoints
|
||||
await secretScanningV2Router.register(registerSecretScanningV2Router);
|
||||
|
||||
// register service-specific secret scanning endpoints (gitlab/github, etc.)
|
||||
for await (const [type, router] of Object.entries(SECRET_SCANNING_REGISTER_ROUTER_MAP)) {
|
||||
await secretScanningV2Router.register(router, { prefix: `data-sources/${type}` });
|
||||
}
|
||||
},
|
||||
{ prefix: "/secret-scanning" }
|
||||
);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-ro
|
||||
import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router";
|
||||
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||
import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router";
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
|
||||
export * from "./secret-rotation-v2-router";
|
||||
@ -15,6 +16,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
> = {
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||
[SecretRotation.MySqlCredentials]: registerMySqlCredentialsRotationRouter,
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
||||
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
|
||||
|
@ -0,0 +1,19 @@
|
||||
import {
|
||||
CreateMySqlCredentialsRotationSchema,
|
||||
MySqlCredentialsRotationSchema,
|
||||
UpdateMySqlCredentialsRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/mysql-credentials";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerMySqlCredentialsRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.MySqlCredentials,
|
||||
server,
|
||||
responseSchema: MySqlCredentialsRotationSchema,
|
||||
createSchema: CreateMySqlCredentialsRotationSchema,
|
||||
updateSchema: UpdateMySqlCredentialsRotationSchema,
|
||||
generatedCredentialsSchema: SqlCredentialsRotationGeneratedCredentialsSchema
|
||||
});
|
@ -6,6 +6,7 @@ import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-
|
||||
import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
|
||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs";
|
||||
@ -16,6 +17,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
MsSqlCredentialsRotationListItemSchema,
|
||||
MySqlCredentialsRotationListItemSchema,
|
||||
Auth0ClientSecretRotationListItemSchema,
|
||||
AzureClientSecretRotationListItemSchema,
|
||||
AwsIamUserSecretRotationListItemSchema,
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { registerSecretScanningEndpoints } from "@app/ee/routes/v2/secret-scanning-v2-routers/secret-scanning-v2-endpoints";
|
||||
import {
|
||||
CreateGitHubDataSourceSchema,
|
||||
GitHubDataSourceSchema,
|
||||
UpdateGitHubDataSourceSchema
|
||||
} from "@app/ee/services/secret-scanning-v2/github";
|
||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
|
||||
export const registerGitHubSecretScanningRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretScanningEndpoints({
|
||||
type: SecretScanningDataSource.GitHub,
|
||||
server,
|
||||
responseSchema: GitHubDataSourceSchema,
|
||||
createSchema: CreateGitHubDataSourceSchema,
|
||||
updateSchema: UpdateGitHubDataSourceSchema
|
||||
});
|
12
backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts
Normal file
12
backend/src/ee/routes/v2/secret-scanning-v2-routers/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { SecretScanningDataSource } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
|
||||
import { registerGitHubSecretScanningRouter } from "./github-secret-scanning-router";
|
||||
|
||||
export * from "./secret-scanning-v2-router";
|
||||
|
||||
export const SECRET_SCANNING_REGISTER_ROUTER_MAP: Record<
|
||||
SecretScanningDataSource,
|
||||
(server: FastifyZodProvider) => Promise<void>
|
||||
> = {
|
||||
[SecretScanningDataSource.GitHub]: registerGitHubSecretScanningRouter
|
||||
};
|
@ -0,0 +1,593 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningResourcesSchema, SecretScanningScansSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningScanStatus
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_NAME_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
|
||||
import {
|
||||
TSecretScanningDataSource,
|
||||
TSecretScanningDataSourceInput
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { ApiDocsTags, SecretScanningDataSources } from "@app/lib/api-docs";
|
||||
import { startsWithVowel } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretScanningEndpoints = <
|
||||
T extends TSecretScanningDataSource,
|
||||
I extends TSecretScanningDataSourceInput
|
||||
>({
|
||||
server,
|
||||
type,
|
||||
createSchema,
|
||||
updateSchema,
|
||||
responseSchema
|
||||
}: {
|
||||
type: SecretScanningDataSource;
|
||||
server: FastifyZodProvider;
|
||||
createSchema: z.ZodType<{
|
||||
name: string;
|
||||
projectId: string;
|
||||
connectionId?: string;
|
||||
config: Partial<I["config"]>;
|
||||
description?: string | null;
|
||||
isAutoScanEnabled?: boolean;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
name?: string;
|
||||
config?: Partial<I["config"]>;
|
||||
description?: string | null;
|
||||
isAutoScanEnabled?: boolean;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
const sourceType = SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type];
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `List the ${sourceType} Data Sources for the specified project.`,
|
||||
querystring: z.object({
|
||||
projectId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Project ID required")
|
||||
.describe(SecretScanningDataSources.LIST(type).projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ dataSources: responseSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId }
|
||||
} = req;
|
||||
|
||||
const dataSources = (await server.services.secretScanningV2.listSecretScanningDataSourcesByProjectId(
|
||||
{ projectId, type },
|
||||
req.permission
|
||||
)) as T[];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST,
|
||||
metadata: {
|
||||
type,
|
||||
count: dataSources.length,
|
||||
dataSourceIds: dataSources.map((source) => source.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSources };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:dataSourceId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Get the specified ${sourceType} Data Source by ID.`,
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.GET_BY_ID(type).dataSourceId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ dataSource: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const dataSource = (await server.services.secretScanningV2.findSecretScanningDataSourceById(
|
||||
{ dataSourceId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_GET,
|
||||
metadata: {
|
||||
dataSourceId,
|
||||
type
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSource };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/data-source-name/:dataSourceName`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Get the specified ${sourceType} Data Source by name and project ID.`,
|
||||
params: z.object({
|
||||
sourceName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Data Source name required")
|
||||
.describe(SecretScanningDataSources.GET_BY_NAME(type).sourceName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Project ID required")
|
||||
.describe(SecretScanningDataSources.GET_BY_NAME(type).projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ dataSource: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { sourceName } = req.params;
|
||||
const { projectId } = req.query;
|
||||
|
||||
const dataSource = (await server.services.secretScanningV2.findSecretScanningDataSourceByName(
|
||||
{ sourceName, projectId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_GET,
|
||||
metadata: {
|
||||
dataSourceId: dataSource.id,
|
||||
type
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSource };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Create ${
|
||||
startsWithVowel(sourceType) ? "an" : "a"
|
||||
} ${sourceType} Data Source for the specified project.`,
|
||||
body: createSchema,
|
||||
response: {
|
||||
200: z.object({ dataSource: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const dataSource = (await server.services.secretScanningV2.createSecretScanningDataSource(
|
||||
{ ...req.body, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_CREATE,
|
||||
metadata: {
|
||||
dataSourceId: dataSource.id,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSource };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:dataSourceId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Update the specified ${sourceType} Data Source.`,
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.UPDATE(type).dataSourceId)
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
200: z.object({ dataSource: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const dataSource = (await server.services.secretScanningV2.updateSecretScanningDataSource(
|
||||
{ ...req.body, dataSourceId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE,
|
||||
metadata: {
|
||||
dataSourceId,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSource };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: `/:dataSourceId`,
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Delete the specified ${sourceType} Data Source.`,
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.DELETE(type).dataSourceId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ dataSource: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const dataSource = (await server.services.secretScanningV2.deleteSecretScanningDataSource(
|
||||
{ type, dataSourceId },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_DELETE,
|
||||
metadata: {
|
||||
type,
|
||||
dataSourceId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSource };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: `/:dataSourceId/scan`,
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Trigger a scan for the specified ${sourceType} Data Source.`,
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.SCAN(type).dataSourceId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ dataSource: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const dataSource = (await server.services.secretScanningV2.triggerSecretScanningDataSourceScan(
|
||||
{ type, dataSourceId },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN,
|
||||
metadata: {
|
||||
type,
|
||||
dataSourceId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSource };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: `/:dataSourceId/resources/:resourceId/scan`,
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Trigger a scan for the specified ${sourceType} Data Source resource.`,
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.SCAN(type).dataSourceId),
|
||||
resourceId: z.string().uuid().describe(SecretScanningDataSources.SCAN(type).resourceId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ dataSource: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId, resourceId } = req.params;
|
||||
|
||||
const dataSource = (await server.services.secretScanningV2.triggerSecretScanningDataSourceScan(
|
||||
{ type, dataSourceId, resourceId },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: dataSource.projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN,
|
||||
metadata: {
|
||||
type,
|
||||
dataSourceId,
|
||||
resourceId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSource };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:dataSourceId/resources",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Get the resources associated with the specified ${sourceType} Data Source by ID.`,
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.LIST_RESOURCES(type).dataSourceId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ resources: SecretScanningResourcesSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const { resources, projectId } = await server.services.secretScanningV2.listSecretScanningResourcesByDataSourceId(
|
||||
{ dataSourceId, type },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_RESOURCE_LIST,
|
||||
metadata: {
|
||||
dataSourceId,
|
||||
type,
|
||||
resourceIds: resources.map((resource) => resource.id),
|
||||
count: resources.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { resources };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:dataSourceId/scans",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: `Get the scans associated with the specified ${sourceType} Data Source by ID.`,
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid().describe(SecretScanningDataSources.LIST_SCANS(type).dataSourceId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ scans: SecretScanningScansSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const { scans, projectId } = await server.services.secretScanningV2.listSecretScanningScansByDataSourceId(
|
||||
{ dataSourceId, type },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_SCAN_LIST,
|
||||
metadata: {
|
||||
dataSourceId,
|
||||
type,
|
||||
count: scans.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { scans };
|
||||
}
|
||||
});
|
||||
|
||||
// not exposed, for UI only
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:dataSourceId/resources-dashboard",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
resources: SecretScanningResourcesSchema.extend({
|
||||
lastScannedAt: z.date().nullish(),
|
||||
lastScanStatus: z.nativeEnum(SecretScanningScanStatus).nullish(),
|
||||
lastScanStatusMessage: z.string().nullish(),
|
||||
unresolvedFindings: z.number()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const { resources, projectId } =
|
||||
await server.services.secretScanningV2.listSecretScanningResourcesWithDetailsByDataSourceId(
|
||||
{ dataSourceId, type },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_RESOURCE_LIST,
|
||||
metadata: {
|
||||
dataSourceId,
|
||||
type,
|
||||
resourceIds: resources.map((resource) => resource.id),
|
||||
count: resources.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { resources };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:dataSourceId/scans-dashboard",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
params: z.object({
|
||||
dataSourceId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
scans: SecretScanningScansSchema.extend({
|
||||
unresolvedFindings: z.number(),
|
||||
resolvedFindings: z.number(),
|
||||
resourceName: z.string()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { dataSourceId } = req.params;
|
||||
|
||||
const { scans, projectId } =
|
||||
await server.services.secretScanningV2.listSecretScanningScansWithDetailsByDataSourceId(
|
||||
{ dataSourceId, type },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_SCAN_LIST,
|
||||
metadata: {
|
||||
dataSourceId,
|
||||
type,
|
||||
count: scans.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { scans };
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,366 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningConfigsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { GitHubDataSourceListItemSchema } from "@app/ee/services/secret-scanning-v2/github";
|
||||
import {
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningScanStatus
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import {
|
||||
SecretScanningDataSourceSchema,
|
||||
SecretScanningFindingSchema
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-union-schemas";
|
||||
import {
|
||||
ApiDocsTags,
|
||||
SecretScanningConfigs,
|
||||
SecretScanningDataSources,
|
||||
SecretScanningFindings
|
||||
} from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SecretScanningDataSourceOptionsSchema = z.discriminatedUnion("type", [GitHubDataSourceListItemSchema]);
|
||||
|
||||
export const registerSecretScanningV2Router = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/data-sources/options",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "List the available Secret Scanning Data Source Options.",
|
||||
response: {
|
||||
200: z.object({
|
||||
dataSourceOptions: SecretScanningDataSourceOptionsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: () => {
|
||||
const dataSourceOptions = server.services.secretScanningV2.listSecretScanningDataSourceOptions();
|
||||
return { dataSourceOptions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/data-sources",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "List all the Secret Scanning Data Sources for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretScanningDataSources.LIST().projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ dataSources: SecretScanningDataSourceSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const dataSources = await server.services.secretScanningV2.listSecretScanningDataSourcesByProjectId(
|
||||
{ projectId },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST,
|
||||
metadata: {
|
||||
dataSourceIds: dataSources.map((dataSource) => dataSource.id),
|
||||
count: dataSources.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSources };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/findings",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "List all the Secret Scanning Findings for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretScanningFindings.LIST.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ findings: SecretScanningFindingSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const findings = await server.services.secretScanningV2.listSecretScanningFindingsByProjectId(
|
||||
projectId,
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_FINDING_LIST,
|
||||
metadata: {
|
||||
findingIds: findings.map((finding) => finding.id),
|
||||
count: findings.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { findings };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/findings/:findingId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "Update the specified Secret Scanning Finding.",
|
||||
params: z.object({
|
||||
findingId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningFindings.UPDATE.findingId)
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.nativeEnum(SecretScanningFindingStatus).optional().describe(SecretScanningFindings.UPDATE.status),
|
||||
remarks: z.string().nullish().describe(SecretScanningFindings.UPDATE.remarks)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ finding: SecretScanningFindingSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { findingId },
|
||||
body,
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { finding, projectId } = await server.services.secretScanningV2.updateSecretScanningFindingById(
|
||||
{ findingId, ...body },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_FINDING_UPDATE,
|
||||
metadata: {
|
||||
findingId,
|
||||
...body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { finding };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/configs",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "Get the Secret Scanning Config for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Project ID required")
|
||||
.describe(SecretScanningConfigs.GET_BY_PROJECT_ID.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
config: z.object({ content: z.string().nullish(), projectId: z.string(), updatedAt: z.date().nullish() })
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const config = await server.services.secretScanningV2.findSecretScanningConfigByProjectId(projectId, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_CONFIG_GET
|
||||
}
|
||||
});
|
||||
|
||||
return { config };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/configs",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "Update the specified Secret Scanning Configuration.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretScanningConfigs.UPDATE.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
content: z.string().nullable().describe(SecretScanningConfigs.UPDATE.content)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ config: SecretScanningConfigsSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
body,
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const config = await server.services.secretScanningV2.upsertSecretScanningConfig(
|
||||
{ projectId, ...body },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_CONFIG_UPDATE,
|
||||
metadata: body
|
||||
}
|
||||
});
|
||||
|
||||
return { config };
|
||||
}
|
||||
});
|
||||
|
||||
// not exposed, for UI only
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/data-sources-dashboard",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
dataSources: z
|
||||
.intersection(
|
||||
SecretScanningDataSourceSchema,
|
||||
z.object({
|
||||
lastScannedAt: z.date().nullish(),
|
||||
lastScanStatus: z.nativeEnum(SecretScanningScanStatus).nullish(),
|
||||
lastScanStatusMessage: z.string().nullish(),
|
||||
unresolvedFindings: z.number().nullish()
|
||||
})
|
||||
)
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const dataSources = await server.services.secretScanningV2.listSecretScanningDataSourcesWithDetailsByProjectId(
|
||||
{ projectId },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST,
|
||||
metadata: {
|
||||
dataSourceIds: dataSources.map((dataSource) => dataSource.id),
|
||||
count: dataSources.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { dataSources };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/unresolved-findings-count",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretScanningFindings.LIST.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ unresolvedFindings: z.number() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const unresolvedFindings =
|
||||
await server.services.secretScanningV2.getSecretScanningUnresolvedFindingsCountByProjectId(
|
||||
projectId,
|
||||
permission
|
||||
);
|
||||
|
||||
return { unresolvedFindings };
|
||||
}
|
||||
});
|
||||
};
|
@ -8,3 +8,10 @@ export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover);
|
||||
return { ...accessApprovalPolicyApproverOrm };
|
||||
};
|
||||
|
||||
export type TAccessApprovalPolicyBypasserDALFactory = ReturnType<typeof accessApprovalPolicyBypasserDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyBypasserDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyBypasserOrm = ormify(db, TableName.AccessApprovalPolicyBypasser);
|
||||
return { ...accessApprovalPolicyBypasserOrm };
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from "@app/db/schemas";
|
||||
import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies, TUsers } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApproverType } from "./access-approval-policy-types";
|
||||
import { ApproverType, BypasserType } from "./access-approval-policy-types";
|
||||
|
||||
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||
|
||||
@ -34,9 +34,22 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyBypasser,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyBypasser}.policyId`
|
||||
)
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("bypasserUsers"),
|
||||
`${TableName.AccessApprovalPolicyBypasser}.bypasserUserId`,
|
||||
`bypasserUsers.id`
|
||||
)
|
||||
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
|
||||
.select(tx.ref("username").withSchema("bypasserUsers").as("bypasserUsername"))
|
||||
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
|
||||
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
@ -129,6 +142,23 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
type: ApproverType.Group
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "bypasserUserId",
|
||||
label: "bypassers" as const,
|
||||
mapper: ({ bypasserUserId: id, bypasserUsername }) => ({
|
||||
id,
|
||||
type: BypasserType.User,
|
||||
name: bypasserUsername
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "bypasserGroupId",
|
||||
label: "bypassers" as const,
|
||||
mapper: ({ bypasserGroupId: id }) => ({
|
||||
id,
|
||||
type: BypasserType.Group
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -144,5 +174,28 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
return softDeletedPolicy;
|
||||
};
|
||||
|
||||
return { ...accessApprovalPolicyOrm, find, findById, softDeleteById };
|
||||
const findLastValidPolicy = async ({ envId, secretPath }: { envId: string; secretPath: string }, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db.replicaNode())(TableName.AccessApprovalPolicy)
|
||||
.where(
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
buildFindFilter(
|
||||
{
|
||||
envId,
|
||||
secretPath
|
||||
},
|
||||
TableName.AccessApprovalPolicy
|
||||
)
|
||||
)
|
||||
.orderBy("deletedAt", "desc")
|
||||
.orderByRaw(`"deletedAt" IS NULL`)
|
||||
.first();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindLastValidPolicy" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalPolicyOrm, find, findById, softDeleteById, findLastValidPolicy };
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
@ -14,10 +15,14 @@ import { TAccessApprovalRequestReviewerDALFactory } from "../access-approval-req
|
||||
import { ApprovalStatus } from "../access-approval-request/access-approval-request-types";
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||
import {
|
||||
TAccessApprovalPolicyApproverDALFactory,
|
||||
TAccessApprovalPolicyBypasserDALFactory
|
||||
} from "./access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||
import {
|
||||
ApproverType,
|
||||
BypasserType,
|
||||
TCreateAccessApprovalPolicy,
|
||||
TDeleteAccessApprovalPolicy,
|
||||
TGetAccessApprovalPolicyByIdDTO,
|
||||
@ -32,12 +37,14 @@ type TAccessApprovalPolicyServiceFactoryDep = {
|
||||
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||
accessApprovalPolicyBypasserDAL: TAccessApprovalPolicyBypasserDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
groupDAL: TGroupDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">;
|
||||
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||
@ -45,6 +52,7 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
|
||||
export const accessApprovalPolicyServiceFactory = ({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
accessApprovalPolicyBypasserDAL,
|
||||
groupDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
@ -52,7 +60,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
userDAL,
|
||||
accessApprovalRequestDAL,
|
||||
additionalPrivilegeDAL,
|
||||
accessApprovalRequestReviewerDAL
|
||||
accessApprovalRequestReviewerDAL,
|
||||
orgMembershipDAL
|
||||
}: TAccessApprovalPolicyServiceFactoryDep) => {
|
||||
const createAccessApprovalPolicy = async ({
|
||||
name,
|
||||
@ -63,6 +72,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
approvers,
|
||||
bypassers,
|
||||
projectSlug,
|
||||
environment,
|
||||
enforcementLevel,
|
||||
@ -82,7 +92,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
|
||||
@ -147,6 +157,44 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
.map((user) => user.id);
|
||||
verifyAllApprovers.push(...verifyGroupApprovers);
|
||||
|
||||
let groupBypassers: string[] = [];
|
||||
let bypasserUserIds: string[] = [];
|
||||
|
||||
if (bypassers && bypassers.length) {
|
||||
groupBypassers = bypassers
|
||||
.filter((bypasser) => bypasser.type === BypasserType.Group)
|
||||
.map((bypasser) => bypasser.id) as string[];
|
||||
|
||||
const userBypassers = bypassers
|
||||
.filter((bypasser) => bypasser.type === BypasserType.User)
|
||||
.map((bypasser) => bypasser.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userBypasserNames = bypassers
|
||||
.map((bypasser) => (bypasser.type === BypasserType.User ? bypasser.username : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
bypasserUserIds = userBypassers;
|
||||
if (userBypasserNames.length) {
|
||||
const bypasserUsers = await userDAL.find({
|
||||
$in: {
|
||||
username: userBypasserNames
|
||||
}
|
||||
});
|
||||
|
||||
const bypasserNamesFromDb = bypasserUsers.map((user) => user.username);
|
||||
const invalidUsernames = userBypasserNames.filter((username) => !bypasserNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid bypasser user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
bypasserUserIds = bypasserUserIds.concat(bypasserUsers.map((user) => user.id));
|
||||
}
|
||||
}
|
||||
|
||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.create(
|
||||
{
|
||||
@ -159,6 +207,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (approverUserIds.length) {
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
approverUserIds.map((userId) => ({
|
||||
@ -179,8 +228,29 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (bypasserUserIds.length) {
|
||||
await accessApprovalPolicyBypasserDAL.insertMany(
|
||||
bypasserUserIds.map((userId) => ({
|
||||
bypasserUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupBypassers.length) {
|
||||
await accessApprovalPolicyBypasserDAL.insertMany(
|
||||
groupBypassers.map((groupId) => ({
|
||||
bypasserGroupId: groupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
|
||||
return { ...accessApproval, environment: env, projectId: project.id };
|
||||
};
|
||||
|
||||
@ -211,6 +281,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
const updateAccessApprovalPolicy = async ({
|
||||
policyId,
|
||||
approvers,
|
||||
bypassers,
|
||||
secretPath,
|
||||
name,
|
||||
actorId,
|
||||
@ -231,15 +302,15 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userApproverNames = approvers
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
|
||||
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
const currentAppovals = approvals || accessApprovalPolicy.approvals;
|
||||
const currentApprovals = approvals || accessApprovalPolicy.approvals;
|
||||
if (
|
||||
groupApprovers?.length === 0 &&
|
||||
userApprovers &&
|
||||
currentAppovals > userApprovers.length + userApproverNames.length
|
||||
currentApprovals > userApprovers.length + userApproverNames.length
|
||||
) {
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
}
|
||||
@ -258,6 +329,78 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
let groupBypassers: string[] = [];
|
||||
let bypasserUserIds: string[] = [];
|
||||
|
||||
if (bypassers && bypassers.length) {
|
||||
groupBypassers = bypassers
|
||||
.filter((bypasser) => bypasser.type === BypasserType.Group)
|
||||
.map((bypasser) => bypasser.id) as string[];
|
||||
|
||||
groupBypassers = [...new Set(groupBypassers)];
|
||||
|
||||
const userBypassers = bypassers
|
||||
.filter((bypasser) => bypasser.type === BypasserType.User)
|
||||
.map((bypasser) => bypasser.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const userBypasserNames = bypassers
|
||||
.map((bypasser) => (bypasser.type === BypasserType.User ? bypasser.username : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
bypasserUserIds = userBypassers;
|
||||
if (userBypasserNames.length) {
|
||||
const bypasserUsers = await userDAL.find({
|
||||
$in: {
|
||||
username: userBypasserNames
|
||||
}
|
||||
});
|
||||
|
||||
const bypasserNamesFromDb = bypasserUsers.map((user) => user.username);
|
||||
const invalidUsernames = userBypasserNames.filter((username) => !bypasserNamesFromDb.includes(username));
|
||||
|
||||
if (invalidUsernames.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid bypasser user: ${invalidUsernames.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
bypasserUserIds = [...new Set(bypasserUserIds.concat(bypasserUsers.map((user) => user.id)))];
|
||||
}
|
||||
|
||||
// Validate user bypassers
|
||||
if (bypasserUserIds.length > 0) {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: { userId: bypasserUserIds },
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (orgMemberships.length !== bypasserUserIds.length) {
|
||||
const foundUserIdsInOrg = new Set(orgMemberships.map((mem) => mem.userId));
|
||||
const missingUserIds = bypasserUserIds.filter((id) => !foundUserIdsInOrg.has(id));
|
||||
throw new BadRequestError({
|
||||
message: `One or more specified bypasser users are not part of the organization or do not exist. Invalid or non-member user IDs: ${missingUserIds.join(", ")}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate group bypassers
|
||||
if (groupBypassers.length > 0) {
|
||||
const orgGroups = await groupDAL.find({
|
||||
$in: { id: groupBypassers },
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (orgGroups.length !== groupBypassers.length) {
|
||||
const foundGroupIdsInOrg = new Set(orgGroups.map((group) => group.id));
|
||||
const missingGroupIds = groupBypassers.filter((id) => !foundGroupIdsInOrg.has(id));
|
||||
throw new BadRequestError({
|
||||
message: `One or more specified bypasser groups are not part of the organization or do not exist. Invalid or non-member group IDs: ${missingGroupIds.join(", ")}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.updateById(
|
||||
accessApprovalPolicy.id,
|
||||
@ -313,6 +456,28 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
|
||||
|
||||
if (bypasserUserIds.length) {
|
||||
await accessApprovalPolicyBypasserDAL.insertMany(
|
||||
bypasserUserIds.map((userId) => ({
|
||||
bypasserUserId: userId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (groupBypassers.length) {
|
||||
await accessApprovalPolicyBypasserDAL.insertMany(
|
||||
groupBypassers.map((groupId) => ({
|
||||
bypasserGroupId: groupId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
|
@ -18,11 +18,20 @@ export enum ApproverType {
|
||||
User = "user"
|
||||
}
|
||||
|
||||
export enum BypasserType {
|
||||
Group = "group",
|
||||
User = "user"
|
||||
}
|
||||
|
||||
export type TCreateAccessApprovalPolicy = {
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
|
||||
bypassers?: (
|
||||
| { type: BypasserType.Group; id: string }
|
||||
| { type: BypasserType.User; id?: string; username?: string }
|
||||
)[];
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
@ -32,7 +41,11 @@ export type TCreateAccessApprovalPolicy = {
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
approvals?: number;
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
|
||||
bypassers?: (
|
||||
| { type: BypasserType.Group; id: string }
|
||||
| { type: BypasserType.User; id?: string; username?: string }
|
||||
)[];
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests, TUsers } from "@app/db/schemas";
|
||||
import {
|
||||
AccessApprovalRequestsSchema,
|
||||
TableName,
|
||||
TAccessApprovalRequests,
|
||||
TUserGroupMembership,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
@ -28,12 +34,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
@ -46,6 +52,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyBypasser,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyBypasser}.policyId`
|
||||
)
|
||||
.leftJoin<TUserGroupMembership>(
|
||||
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
|
||||
`${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`,
|
||||
`bypasserUserGroupMembership.groupId`
|
||||
)
|
||||
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("requestedByUser"),
|
||||
`${TableName.AccessApprovalRequest}.requestedByUserId`,
|
||||
@ -69,6 +86,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
|
||||
|
||||
.select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
|
||||
.select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"))
|
||||
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
@ -145,7 +165,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
: null,
|
||||
|
||||
isApproved: !!doc.policyDeletedAt || !!doc.privilegeId
|
||||
isApproved: !!doc.policyDeletedAt || !!doc.privilegeId || doc.status !== ApprovalStatus.PENDING
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
@ -158,6 +178,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
key: "approverGroupUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverGroupUserId }) => approverGroupUserId
|
||||
},
|
||||
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
||||
{
|
||||
key: "bypasserGroupUserId",
|
||||
label: "bypassers" as const,
|
||||
mapper: ({ bypasserGroupUserId }) => bypasserGroupUserId
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -166,7 +192,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
|
||||
return formattedDocs.map((doc) => ({
|
||||
...doc,
|
||||
policy: { ...doc.policy, approvers: doc.approvers }
|
||||
policy: { ...doc.policy, approvers: doc.approvers, bypassers: doc.bypassers }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
|
||||
@ -193,7 +219,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||
@ -204,13 +229,33 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
|
||||
`${TableName.UserGroupMembership}.groupId`
|
||||
)
|
||||
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
|
||||
`${TableName.UserGroupMembership}.userId`,
|
||||
"accessApprovalPolicyGroupApproverUser.id"
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyBypasser,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyBypasser}.policyId`
|
||||
)
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyBypasserUser"),
|
||||
`${TableName.AccessApprovalPolicyBypasser}.bypasserUserId`,
|
||||
"accessApprovalPolicyBypasserUser.id"
|
||||
)
|
||||
.leftJoin<TUserGroupMembership>(
|
||||
db(TableName.UserGroupMembership).as("bypasserUserGroupMembership"),
|
||||
`${TableName.AccessApprovalPolicyBypasser}.bypasserGroupId`,
|
||||
`bypasserUserGroupMembership.groupId`
|
||||
)
|
||||
.leftJoin<TUsers>(
|
||||
db(TableName.Users).as("accessApprovalPolicyGroupBypasserUser"),
|
||||
`bypasserUserGroupMembership.userId`,
|
||||
"accessApprovalPolicyGroupBypasserUser.id"
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
@ -241,6 +286,18 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
|
||||
tx.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"),
|
||||
|
||||
// Bypassers
|
||||
tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser),
|
||||
tx.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"),
|
||||
tx.ref("email").withSchema("accessApprovalPolicyBypasserUser").as("bypasserEmail"),
|
||||
tx.ref("email").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupEmail"),
|
||||
tx.ref("username").withSchema("accessApprovalPolicyBypasserUser").as("bypasserUsername"),
|
||||
tx.ref("username").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupUsername"),
|
||||
tx.ref("firstName").withSchema("accessApprovalPolicyBypasserUser").as("bypasserFirstName"),
|
||||
tx.ref("firstName").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupFirstName"),
|
||||
tx.ref("lastName").withSchema("accessApprovalPolicyBypasserUser").as("bypasserLastName"),
|
||||
tx.ref("lastName").withSchema("accessApprovalPolicyGroupBypasserUser").as("bypasserGroupLastName"),
|
||||
|
||||
tx.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer),
|
||||
|
||||
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
|
||||
@ -265,7 +322,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode());
|
||||
const docs = await sql;
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
@ -335,13 +392,51 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "bypasserUserId",
|
||||
label: "bypassers" as const,
|
||||
mapper: ({
|
||||
bypasserUserId,
|
||||
bypasserEmail: email,
|
||||
bypasserUsername: username,
|
||||
bypasserLastName: lastName,
|
||||
bypasserFirstName: firstName
|
||||
}) => ({
|
||||
userId: bypasserUserId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "bypasserGroupUserId",
|
||||
label: "bypassers" as const,
|
||||
mapper: ({
|
||||
userId,
|
||||
bypasserGroupEmail: email,
|
||||
bypasserGroupUsername: username,
|
||||
bypasserGroupLastName: lastName,
|
||||
bypasserFirstName: firstName
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!formatedDoc?.[0]) return;
|
||||
if (!formattedDoc?.[0]) return;
|
||||
return {
|
||||
...formatedDoc[0],
|
||||
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
|
||||
...formattedDoc[0],
|
||||
policy: {
|
||||
...formattedDoc[0].policy,
|
||||
approvers: formattedDoc[0].approvers,
|
||||
bypassers: formattedDoc[0].bypassers
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" });
|
||||
@ -392,14 +487,20 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
]
|
||||
});
|
||||
|
||||
// an approval is pending if there is no reviewer rejections and no privilege ID is set
|
||||
// an approval is pending if there is no reviewer rejections, no privilege ID is set and the status is pending
|
||||
const pendingApprovals = formattedRequests.filter(
|
||||
(req) => !req.privilegeId && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
(req) =>
|
||||
!req.privilegeId &&
|
||||
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
|
||||
req.status === ApprovalStatus.PENDING
|
||||
);
|
||||
|
||||
// an approval is finalized if there are any rejections or a privilege ID is set
|
||||
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required
|
||||
const finalizedApprovals = formattedRequests.filter(
|
||||
(req) => req.privilegeId || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
(req) =>
|
||||
req.privilegeId ||
|
||||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
|
||||
req.status !== ApprovalStatus.PENDING
|
||||
);
|
||||
|
||||
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
||||
|
@ -6,6 +6,7 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -55,7 +56,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
| "findOne"
|
||||
| "getCount"
|
||||
>;
|
||||
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find">;
|
||||
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find" | "findLastValidPolicy">;
|
||||
accessApprovalRequestReviewerDAL: Pick<
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
@ -130,7 +131,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
|
||||
|
||||
const policy = await accessApprovalPolicyDAL.findOne({
|
||||
const policy = await accessApprovalPolicyDAL.findLastValidPolicy({
|
||||
envId: environment.id,
|
||||
secretPath
|
||||
});
|
||||
@ -202,7 +203,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED);
|
||||
|
||||
if (!isRejected) {
|
||||
if (!isRejected && duplicateRequest.status === ApprovalStatus.PENDING) {
|
||||
throw new BadRequestError({ message: "You already have a pending access request with the same criteria" });
|
||||
}
|
||||
}
|
||||
@ -323,24 +324,20 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
status,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
bypassReason
|
||||
}: TReviewAccessRequestDTO) => {
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
if (!accessApprovalRequest) {
|
||||
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
|
||||
}
|
||||
|
||||
const { policy } = accessApprovalRequest;
|
||||
const { policy, environment } = accessApprovalRequest;
|
||||
if (policy.deletedAt) {
|
||||
throw new BadRequestError({
|
||||
message: "The policy associated with this access request has been deleted."
|
||||
});
|
||||
}
|
||||
if (!policy.allowedSelfApprovals && actorId === accessApprovalRequest.requestedByUserId) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to review access approval request. Users are not authorized to review their own request."
|
||||
});
|
||||
}
|
||||
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -355,29 +352,71 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const isSelfApproval = actorId === accessApprovalRequest.requestedByUserId;
|
||||
const isSoftEnforcement = policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId);
|
||||
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
|
||||
|
||||
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
|
||||
|
||||
// If user is (not an approver OR cant self approve) AND can't bypass policy
|
||||
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to review access approval request. Users are not authorized to review their own request."
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
|
||||
!policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver
|
||||
!isApprover // The request isn't performed by an assigned approver
|
||||
) {
|
||||
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
|
||||
}
|
||||
|
||||
const project = await projectDAL.findById(accessApprovalRequest.projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "The project associated with this access request was not found." });
|
||||
}
|
||||
|
||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
||||
}
|
||||
|
||||
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
|
||||
const review = await accessApprovalRequestReviewerDAL.findOne(
|
||||
const isBreakGlassApprovalAttempt =
|
||||
policy.enforcementLevel === EnforcementLevel.Soft &&
|
||||
actorId === accessApprovalRequest.requestedByUserId &&
|
||||
status === ApprovalStatus.APPROVED;
|
||||
|
||||
let reviewForThisActorProcessing: {
|
||||
id: string;
|
||||
requestId: string;
|
||||
reviewerUserId: string;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
const existingReviewByActorInTx = await accessApprovalRequestReviewerDAL.findOne(
|
||||
{
|
||||
requestId: accessApprovalRequest.id,
|
||||
reviewerUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!review) {
|
||||
const newReview = await accessApprovalRequestReviewerDAL.create(
|
||||
|
||||
// Check if review exists for actor
|
||||
if (existingReviewByActorInTx) {
|
||||
// Check if breakglass re-approval
|
||||
if (isBreakGlassApprovalAttempt && existingReviewByActorInTx.status === ApprovalStatus.APPROVED) {
|
||||
reviewForThisActorProcessing = existingReviewByActorInTx;
|
||||
} else {
|
||||
throw new BadRequestError({ message: "You have already reviewed this request" });
|
||||
}
|
||||
} else {
|
||||
reviewForThisActorProcessing = await accessApprovalRequestReviewerDAL.create(
|
||||
{
|
||||
status,
|
||||
requestId: accessApprovalRequest.id,
|
||||
@ -385,19 +424,26 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const allReviews = [...existingReviews, newReview];
|
||||
const otherReviews = existingReviews.filter((er) => er.reviewerUserId !== actorId);
|
||||
const allUniqueReviews = [...otherReviews, reviewForThisActorProcessing];
|
||||
|
||||
const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
|
||||
const approvedReviews = allUniqueReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
|
||||
const meetsStandardApprovalThreshold = approvedReviews.length >= policy.approvals;
|
||||
|
||||
// approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved.
|
||||
if (approvedReviews.length === policy.approvals) {
|
||||
if (
|
||||
reviewForThisActorProcessing.status === ApprovalStatus.APPROVED &&
|
||||
(meetsStandardApprovalThreshold || isBreakGlassApprovalAttempt)
|
||||
) {
|
||||
const currentRequestState = await accessApprovalRequestDAL.findById(accessApprovalRequest.id, tx);
|
||||
let privilegeIdToSet = currentRequestState?.privilegeId || null;
|
||||
|
||||
if (!privilegeIdToSet) {
|
||||
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
throw new BadRequestError({ message: "Temporary range is required for temporary access" });
|
||||
}
|
||||
|
||||
let privilegeId: string | null = null;
|
||||
|
||||
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
// Permanent access
|
||||
const privilege = await additionalPrivilegeDAL.create(
|
||||
@ -409,7 +455,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
privilegeId = privilege.id;
|
||||
privilegeIdToSet = privilege.id;
|
||||
} else {
|
||||
// Temporary access
|
||||
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
|
||||
@ -421,23 +467,61 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions),
|
||||
isTemporary: true,
|
||||
isTemporary: true, // Explicitly set to true for the privilege
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
|
||||
temporaryRange: accessApprovalRequest.temporaryRange!,
|
||||
temporaryAccessStartTime: startTime,
|
||||
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
temporaryAccessEndTime: new Date(startTime.getTime() + relativeTempAllocatedTimeInMs)
|
||||
},
|
||||
tx
|
||||
);
|
||||
privilegeId = privilege.id;
|
||||
privilegeIdToSet = privilege.id;
|
||||
}
|
||||
|
||||
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx);
|
||||
await accessApprovalRequestDAL.updateById(
|
||||
accessApprovalRequest.id,
|
||||
{ privilegeId: privilegeIdToSet, status: ApprovalStatus.APPROVED },
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return newReview;
|
||||
}
|
||||
throw new BadRequestError({ message: "You have already reviewed this request" });
|
||||
|
||||
// Send notification if this was a breakglass approval
|
||||
if (isBreakGlassApprovalAttempt) {
|
||||
const cfg = getConfig();
|
||||
const actingUser = await userDAL.findById(actorId, tx);
|
||||
|
||||
if (actingUser) {
|
||||
const policyApproverUserIds = policy.approvers
|
||||
.map((ap) => ap.userId)
|
||||
.filter((id): id is string => typeof id === "string");
|
||||
|
||||
if (policyApproverUserIds.length > 0) {
|
||||
const approverUsersForEmail = await userDAL.find({ $in: { id: policyApproverUserIds } }, { tx });
|
||||
const recipientEmails = approverUsersForEmail
|
||||
.map((appUser) => appUser.email)
|
||||
.filter((email): email is string => !!email);
|
||||
|
||||
if (recipientEmails.length > 0) {
|
||||
await smtpService.sendMail({
|
||||
recipients: recipientEmails,
|
||||
subjectLine: "Infisical Secret Access Policy Bypassed",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName: `${actingUser.firstName} ${actingUser.lastName}`,
|
||||
requesterEmail: actingUser.email,
|
||||
bypassReason: bypassReason || "No reason provided",
|
||||
secretPath: policy.secretPath || "/",
|
||||
environment,
|
||||
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`,
|
||||
requestType: "access"
|
||||
},
|
||||
template: SmtpTemplates.AccessSecretRequestBypassed
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviewForThisActorProcessing;
|
||||
});
|
||||
|
||||
return reviewStatus;
|
||||
|
@ -17,6 +17,8 @@ export type TGetAccessRequestCountDTO = {
|
||||
export type TReviewAccessRequestDTO = {
|
||||
requestId: string;
|
||||
status: ApprovalStatus;
|
||||
envName?: string;
|
||||
bypassReason?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAccessApprovalRequestDTO = {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { AppConnection } from "../../../../services/app-connection/app-connection-enums";
|
||||
import { TLicenseServiceFactory } from "../../license/license-service";
|
||||
import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
|
||||
import { TOCIConnection } from "./oci-connection-types";
|
||||
|
||||
@ -22,8 +24,23 @@ type TListOCIVaultKeysDTO = {
|
||||
vaultOcid: string;
|
||||
};
|
||||
|
||||
export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
// Enterprise check
|
||||
export const checkPlan = async (licenseService: Pick<TLicenseServiceFactory, "getPlan">, orgId: string) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.enterpriseAppConnections)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to use app connection due to plan restriction. Upgrade plan to access enterprise app connections."
|
||||
});
|
||||
};
|
||||
|
||||
export const ociConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">
|
||||
) => {
|
||||
const listCompartments = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
await checkPlan(licenseService, actor.orgId);
|
||||
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
@ -36,6 +53,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
||||
};
|
||||
|
||||
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
|
||||
await checkPlan(licenseService, actor.orgId);
|
||||
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
@ -51,6 +70,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
||||
{ connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
await checkPlan(licenseService, actor.orgId);
|
||||
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
@ -2,7 +2,7 @@ import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { AppConnection } from "../../../../services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateOCIConnectionSchema,
|
||||
OCIConnectionSchema,
|
@ -1,3 +1,4 @@
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import {
|
||||
TCreateProjectTemplateDTO,
|
||||
TUpdateProjectTemplateDTO
|
||||
@ -9,6 +10,18 @@ import {
|
||||
TSecretRotationV2Raw,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningScanStatus,
|
||||
SecretScanningScanType
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||
import {
|
||||
TCreateSecretScanningDataSourceDTO,
|
||||
TDeleteSecretScanningDataSourceDTO,
|
||||
TTriggerSecretScanningDataSourceDTO,
|
||||
TUpdateSecretScanningDataSourceDTO,
|
||||
TUpdateSecretScanningFindingDTO
|
||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
@ -20,7 +33,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
|
||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
@ -231,6 +244,7 @@ export enum EventType {
|
||||
REMOVE_HOST_FROM_SSH_HOST_GROUP = "remove-host-from-ssh-host-group",
|
||||
CREATE_CA = "create-certificate-authority",
|
||||
GET_CA = "get-certificate-authority",
|
||||
GET_CAS = "get-certificate-authorities",
|
||||
UPDATE_CA = "update-certificate-authority",
|
||||
DELETE_CA = "delete-certificate-authority",
|
||||
RENEW_CA = "renew-certificate-authority",
|
||||
@ -241,6 +255,7 @@ export enum EventType {
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRLS = "get-certificate-authority-crls",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
IMPORT_CERT = "import-cert",
|
||||
SIGN_CERT = "sign-cert",
|
||||
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
|
||||
GET_CERT = "get-cert",
|
||||
@ -266,7 +281,9 @@ export enum EventType {
|
||||
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
|
||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
|
||||
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
@ -315,7 +332,6 @@ export enum EventType {
|
||||
CREATE_PROJECT_TEMPLATE = "create-project-template",
|
||||
UPDATE_PROJECT_TEMPLATE = "update-project-template",
|
||||
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
||||
APPLY_PROJECT_TEMPLATE = "apply-project-template",
|
||||
GET_APP_CONNECTIONS = "get-app-connections",
|
||||
GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details",
|
||||
GET_APP_CONNECTION = "get-app-connection",
|
||||
@ -375,7 +391,27 @@ export enum EventType {
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end",
|
||||
|
||||
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
|
||||
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
|
||||
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
||||
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
|
||||
SECRET_SCANNING_DATA_SOURCE_GET = "secret-scanning-data-source-get",
|
||||
SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN = "secret-scanning-data-source-trigger-scan",
|
||||
SECRET_SCANNING_DATA_SOURCE_SCAN = "secret-scanning-data-source-scan",
|
||||
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
|
||||
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
|
||||
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
|
||||
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update",
|
||||
SECRET_SCANNING_CONFIG_GET = "secret-scanning-config-get",
|
||||
SECRET_SCANNING_CONFIG_UPDATE = "secret-scanning-config-update",
|
||||
|
||||
UPDATE_ORG = "update-org",
|
||||
|
||||
CREATE_PROJECT = "create-project",
|
||||
UPDATE_PROJECT = "update-project",
|
||||
DELETE_PROJECT = "delete-project"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@ -1772,7 +1808,8 @@ interface CreateCa {
|
||||
type: EventType.CREATE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1780,7 +1817,15 @@ interface GetCa {
|
||||
type: EventType.GET_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCAs {
|
||||
type: EventType.GET_CAS;
|
||||
metadata: {
|
||||
caIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@ -1788,7 +1833,8 @@ interface UpdateCa {
|
||||
type: EventType.UPDATE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
status: CaStatus;
|
||||
};
|
||||
}
|
||||
@ -1797,7 +1843,8 @@ interface DeleteCa {
|
||||
type: EventType.DELETE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
name: string;
|
||||
dn?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1867,6 +1914,15 @@ interface IssueCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportCert {
|
||||
type: EventType.IMPORT_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignCert {
|
||||
type: EventType.SIGN_CERT;
|
||||
metadata: {
|
||||
@ -2034,7 +2090,7 @@ interface CreatePkiSubscriber {
|
||||
caId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
ttl: string;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
@ -2076,7 +2132,15 @@ interface IssuePkiSubscriberCert {
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
serialNumber: string;
|
||||
serialNumber?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AutomatedRenewPkiSubscriberCert {
|
||||
type: EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -2098,6 +2162,16 @@ interface ListPkiSubscriberCerts {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSubscriberActiveCertBundle {
|
||||
type: EventType.GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
certId: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
@ -2451,14 +2525,6 @@ interface DeleteProjectTemplateEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface ApplyProjectTemplateEvent {
|
||||
type: EventType.APPLY_PROJECT_TEMPLATE;
|
||||
metadata: {
|
||||
template: string;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAppConnectionsEvent {
|
||||
type: EventType.GET_APP_CONNECTIONS;
|
||||
metadata: {
|
||||
@ -2913,6 +2979,154 @@ interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceListEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST;
|
||||
metadata: {
|
||||
type?: SecretScanningDataSource;
|
||||
count: number;
|
||||
dataSourceIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceGetEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_GET;
|
||||
metadata: {
|
||||
type: SecretScanningDataSource;
|
||||
dataSourceId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceCreateEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_CREATE;
|
||||
metadata: Omit<TCreateSecretScanningDataSourceDTO, "projectId"> & { dataSourceId: string };
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceUpdateEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE;
|
||||
metadata: TUpdateSecretScanningDataSourceDTO;
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceDeleteEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_DELETE;
|
||||
metadata: TDeleteSecretScanningDataSourceDTO;
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceTriggerScanEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN;
|
||||
metadata: TTriggerSecretScanningDataSourceDTO;
|
||||
}
|
||||
|
||||
interface SecretScanningDataSourceScanEvent {
|
||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN;
|
||||
metadata: {
|
||||
scanId: string;
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
dataSourceId: string;
|
||||
dataSourceType: string;
|
||||
scanStatus: SecretScanningScanStatus;
|
||||
scanType: SecretScanningScanType;
|
||||
numberOfSecretsDetected?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningResourceListEvent {
|
||||
type: EventType.SECRET_SCANNING_RESOURCE_LIST;
|
||||
metadata: {
|
||||
type: SecretScanningDataSource;
|
||||
dataSourceId: string;
|
||||
resourceIds: string[];
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningScanListEvent {
|
||||
type: EventType.SECRET_SCANNING_SCAN_LIST;
|
||||
metadata: {
|
||||
type: SecretScanningDataSource;
|
||||
dataSourceId: string;
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningFindingListEvent {
|
||||
type: EventType.SECRET_SCANNING_FINDING_LIST;
|
||||
metadata: {
|
||||
findingIds: string[];
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningFindingUpdateEvent {
|
||||
type: EventType.SECRET_SCANNING_FINDING_UPDATE;
|
||||
metadata: TUpdateSecretScanningFindingDTO;
|
||||
}
|
||||
|
||||
interface SecretScanningConfigUpdateEvent {
|
||||
type: EventType.SECRET_SCANNING_CONFIG_UPDATE;
|
||||
metadata: {
|
||||
content: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretScanningConfigReadEvent {
|
||||
type: EventType.SECRET_SCANNING_CONFIG_GET;
|
||||
metadata?: Record<string, never>; // not needed, based off projectId
|
||||
}
|
||||
|
||||
interface OrgUpdateEvent {
|
||||
type: EventType.UPDATE_ORG;
|
||||
metadata: {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
authEnforced?: boolean;
|
||||
scimEnabled?: boolean;
|
||||
defaultMembershipRoleSlug?: string;
|
||||
enforceMfa?: boolean;
|
||||
selectedMfaMethod?: string;
|
||||
allowSecretSharingOutsideOrganization?: boolean;
|
||||
bypassOrgAuthEnabled?: boolean;
|
||||
userTokenExpiration?: string;
|
||||
secretsProductEnabled?: boolean;
|
||||
pkiProductEnabled?: boolean;
|
||||
kmsProductEnabled?: boolean;
|
||||
sshProductEnabled?: boolean;
|
||||
scannerProductEnabled?: boolean;
|
||||
shareSecretsProductEnabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectCreateEvent {
|
||||
type: EventType.CREATE_PROJECT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug?: string;
|
||||
type: ProjectType;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectUpdateEvent {
|
||||
type: EventType.UPDATE_PROJECT;
|
||||
metadata: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
autoCapitalization?: boolean;
|
||||
hasDeleteProtection?: boolean;
|
||||
slug?: string;
|
||||
secretSharing?: boolean;
|
||||
pitVersionLimit?: number;
|
||||
auditLogsRetentionDays?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectDeleteEvent {
|
||||
type: EventType.DELETE_PROJECT;
|
||||
metadata: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -3037,6 +3251,7 @@ export type Event =
|
||||
| IssueSshHostHostCert
|
||||
| CreateCa
|
||||
| GetCa
|
||||
| GetCAs
|
||||
| UpdateCa
|
||||
| DeleteCa
|
||||
| RenewCa
|
||||
@ -3047,6 +3262,7 @@ export type Event =
|
||||
| ImportCaCert
|
||||
| GetCaCrls
|
||||
| IssueCert
|
||||
| ImportCert
|
||||
| SignCert
|
||||
| GetCaCertificateTemplates
|
||||
| GetCert
|
||||
@ -3072,7 +3288,9 @@ export type Event =
|
||||
| GetPkiSubscriber
|
||||
| IssuePkiSubscriberCert
|
||||
| SignPkiSubscriberCert
|
||||
| AutomatedRenewPkiSubscriberCert
|
||||
| ListPkiSubscriberCerts
|
||||
| GetSubscriberActiveCertBundle
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
@ -3117,7 +3335,6 @@ export type Event =
|
||||
| CreateProjectTemplateEvent
|
||||
| UpdateProjectTemplateEvent
|
||||
| DeleteProjectTemplateEvent
|
||||
| ApplyProjectTemplateEvent
|
||||
| GetAppConnectionsEvent
|
||||
| GetAvailableAppConnectionsDetailsEvent
|
||||
| GetAppConnectionEvent
|
||||
@ -3179,4 +3396,21 @@ export type Event =
|
||||
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
||||
| SecretScanningDataSourceListEvent
|
||||
| SecretScanningDataSourceGetEvent
|
||||
| SecretScanningDataSourceCreateEvent
|
||||
| SecretScanningDataSourceUpdateEvent
|
||||
| SecretScanningDataSourceDeleteEvent
|
||||
| SecretScanningDataSourceTriggerScanEvent
|
||||
| SecretScanningDataSourceScanEvent
|
||||
| SecretScanningResourceListEvent
|
||||
| SecretScanningScanListEvent
|
||||
| SecretScanningFindingListEvent
|
||||
| SecretScanningFindingUpdateEvent
|
||||
| SecretScanningConfigUpdateEvent
|
||||
| SecretScanningConfigReadEvent
|
||||
| OrgUpdateEvent
|
||||
| ProjectCreateEvent
|
||||
| ProjectUpdateEvent
|
||||
| ProjectDeleteEvent;
|
||||
|
@ -7,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
@ -14,7 +15,7 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
|
||||
import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
|
||||
|
||||
type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
@ -37,7 +38,8 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
|
||||
if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` });
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caCrl.caId);
|
||||
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caCrl.caId}' not found` });
|
||||
|
||||
const keyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
@ -54,7 +56,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
|
||||
return {
|
||||
ca,
|
||||
ca: expandInternalCa(ca),
|
||||
caCrl,
|
||||
crl: crl.rawData
|
||||
};
|
||||
@ -64,8 +66,8 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
* Returns a list of CRL ids for CA with id [caId]
|
||||
*/
|
||||
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -108,7 +110,7 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
);
|
||||
|
||||
return {
|
||||
ca,
|
||||
ca: expandInternalCa(ca),
|
||||
crls: decryptedCrls
|
||||
};
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -16,10 +16,10 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
|
||||
|
||||
type TCertificateEstServiceFactoryDep = {
|
||||
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
@ -29,7 +29,7 @@ type TCertificateEstServiceFactoryDep = {
|
||||
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
|
||||
|
||||
export const certificateEstServiceFactory = ({
|
||||
certificateAuthorityService,
|
||||
internalCertificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
@ -127,7 +127,7 @@ export const certificateEstServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
@ -188,7 +188,7 @@ export const certificateEstServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
@ -227,15 +227,15 @@ export const certificateEstServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
|
||||
if (!ca) {
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId);
|
||||
if (!ca?.internalCa?.id) {
|
||||
throw new NotFoundError({
|
||||
message: `Certificate Authority with ID '${certTemplate.caId}' not found`
|
||||
message: `Internal Certificate Authority with ID '${certTemplate.caId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId as string,
|
||||
caCertId: ca.internalCa.activeCaCertId as string,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
|
@ -132,7 +132,11 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
|
||||
result = await selectedProvider.create({
|
||||
inputs: decryptedStoredInput,
|
||||
expireAt: expireAt.getTime(),
|
||||
usernameTemplate: dynamicSecretCfg.usernameTemplate
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||
throw new BadRequestError({ message: error.sqlMessage as string });
|
||||
|
@ -11,6 +11,8 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
||||
|
||||
if (appCfg.isDevelopmentMode) return [host];
|
||||
|
||||
if (isGateway) return [host];
|
||||
|
||||
const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat(
|
||||
(appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)),
|
||||
getDbConnectionHost(appCfg.REDIS_URL),
|
||||
@ -58,7 +60,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
||||
}
|
||||
}
|
||||
|
||||
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
|
||||
if (!(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
@ -78,7 +78,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorOrgId,
|
||||
defaultTTL,
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
metadata,
|
||||
usernameTemplate
|
||||
}: TCreateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@ -163,7 +164,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
gatewayId: selectedGatewayId
|
||||
gatewayId: selectedGatewayId,
|
||||
usernameTemplate
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -199,7 +201,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
newName,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
metadata,
|
||||
usernameTemplate
|
||||
}: TUpdateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@ -311,7 +314,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
gatewayId: selectedGatewayId
|
||||
gatewayId: selectedGatewayId,
|
||||
usernameTemplate
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@ -22,6 +22,7 @@ export type TCreateDynamicSecretDTO = {
|
||||
name: string;
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
usernameTemplate?: string | null;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateDynamicSecretDTO = {
|
||||
@ -34,6 +35,7 @@ export type TUpdateDynamicSecretDTO = {
|
||||
inputs?: TProvider["inputs"];
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
usernameTemplate?: string | null;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteDynamicSecretDTO = {
|
||||
|
@ -132,9 +132,15 @@ const generatePassword = () => {
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
|
||||
return `inf-${customAlphabet(charset, 32)()}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
const randomUsername = `inf-${customAlphabet(charset, 32)()}`;
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
@ -168,13 +174,14 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!(await validateConnection(providerInputs))) {
|
||||
throw new BadRequestError({ message: "Failed to establish connection" });
|
||||
}
|
||||
|
||||
const leaseUsername = generateUsername();
|
||||
const leaseUsername = generateUsername(usernameTemplate);
|
||||
const leasePassword = generatePassword();
|
||||
const leaseExpiration = new Date(expireAt).toISOString();
|
||||
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
PutUserPolicyCommand,
|
||||
RemoveUserFromGroupCommand
|
||||
} from "@aws-sdk/client-iam";
|
||||
import handlebars from "handlebars";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@ -23,8 +24,14 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
@ -53,11 +60,13 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
|
@ -55,7 +55,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
return data.success;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const create = async ({ inputs }: { inputs: unknown }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
@ -88,7 +88,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
// Creates a new password
|
||||
await create(inputs);
|
||||
await create({ inputs });
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
|
@ -14,8 +14,14 @@ const generatePassword = (size = 48) => {
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
@ -69,11 +75,12 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
const { keyspace } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -12,8 +13,14 @@ const generatePassword = () => {
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
@ -64,11 +71,12 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
return infoResponse;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
|
||||
await connection.security.putUser({
|
||||
|
@ -6,6 +6,7 @@ import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { KubernetesProvider } from "./kubernetes";
|
||||
import { LdapProvider } from "./ldap";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||
@ -16,6 +17,7 @@ import { SapAseProvider } from "./sap-ase";
|
||||
import { SapHanaProvider } from "./sap-hana";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
import { TotpProvider } from "./totp";
|
||||
import { VerticaProvider } from "./vertica";
|
||||
|
||||
type TBuildDynamicSecretProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
@ -38,5 +40,7 @@ export const buildDynamicSecretProviders = ({
|
||||
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||
[DynamicSecretProviders.SapAse]: SapAseProvider()
|
||||
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService })
|
||||
});
|
||||
|
200
backend/src/ee/services/dynamic-secret/providers/kubernetes.ts
Normal file
200
backend/src/ee/services/dynamic-secret/providers/kubernetes.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
import { InternalServerError } from "@app/lib/errors";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { DynamicSecretKubernetesSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
type TKubernetesProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
};
|
||||
|
||||
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||
if (!providerInputs.gatewayId) {
|
||||
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $gatewayProxyWrapper = async <T>(
|
||||
inputs: {
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
},
|
||||
gatewayCallback: (host: string, port: number) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
|
||||
const callbackResult = await withGatewayProxy(
|
||||
async (port) => {
|
||||
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
|
||||
const res = await gatewayCallback("https://localhost", port);
|
||||
return res;
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost: inputs.targetHost,
|
||||
targetPort: inputs.targetPort,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
identityId: relayDetails.identityId,
|
||||
orgId: relayDetails.orgId,
|
||||
tlsOptions: {
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey.toString()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return callbackResult;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const serviceAccountGetCallback = async (host: string, port: number) => {
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
|
||||
await axios.get(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${providerInputs.serviceAccountName}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${providerInputs.clusterToken}`
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent: new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: providerInputs.sslEnabled
|
||||
})
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const url = new URL(providerInputs.url);
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
|
||||
try {
|
||||
if (providerInputs.gatewayId) {
|
||||
const k8sHost = url.hostname;
|
||||
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort
|
||||
},
|
||||
serviceAccountGetCallback
|
||||
);
|
||||
} else {
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
await serviceAccountGetCallback(k8sHost, k8sPort);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to validate connection: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async ({ inputs, expireAt }: { inputs: unknown; expireAt: number }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const tokenRequestCallback = async (host: string, port: number) => {
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
|
||||
const res = await axios.post<TKubernetesTokenRequest>(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${providerInputs.serviceAccountName}/token`,
|
||||
{
|
||||
spec: {
|
||||
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
||||
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${providerInputs.clusterToken}`
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent: new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: providerInputs.sslEnabled
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const url = new URL(providerInputs.url);
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
const k8sGatewayHost = url.hostname;
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
|
||||
try {
|
||||
const tokenData = providerInputs.gatewayId
|
||||
? await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort
|
||||
},
|
||||
tokenRequestCallback
|
||||
)
|
||||
: await tokenRequestCallback(k8sHost, k8sPort);
|
||||
|
||||
return {
|
||||
entityId: providerInputs.serviceAccountName,
|
||||
data: { TOKEN: tokenData.status.token }
|
||||
};
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to create dynamic secret: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (_inputs: unknown, entityId: string) => {
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
// No renewal necessary
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -22,8 +22,14 @@ const encodePassword = (password?: string) => {
|
||||
return base64Password;
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(20);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
const generateLDIF = ({
|
||||
@ -190,7 +196,8 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
return dnArray;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
@ -217,7 +224,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
||||
|
||||
|
@ -16,7 +16,8 @@ export enum SqlProviders {
|
||||
MySQL = "mysql2",
|
||||
Oracle = "oracledb",
|
||||
MsSQL = "mssql",
|
||||
SapAse = "sap-ase"
|
||||
SapAse = "sap-ase",
|
||||
Vertica = "vertica"
|
||||
}
|
||||
|
||||
export enum ElasticSearchAuthTypes {
|
||||
@ -29,6 +30,10 @@ export enum LdapCredentialType {
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
export enum KubernetesCredentialType {
|
||||
Static = "static"
|
||||
}
|
||||
|
||||
export enum TotpConfigType {
|
||||
URL = "url",
|
||||
MANUAL = "manual"
|
||||
@ -277,6 +282,51 @@ export const LdapSchema = z.union([
|
||||
})
|
||||
]);
|
||||
|
||||
export const DynamicSecretKubernetesSchema = z.object({
|
||||
url: z.string().url().trim().min(1),
|
||||
gatewayId: z.string().nullable().optional(),
|
||||
sslEnabled: z.boolean().default(true),
|
||||
clusterToken: z.string().trim().min(1),
|
||||
ca: z.string().optional(),
|
||||
serviceAccountName: z.string().trim().min(1),
|
||||
credentialType: z.literal(KubernetesCredentialType.Static),
|
||||
namespace: z.string().trim().min(1),
|
||||
audiences: z.array(z.string().trim().min(1))
|
||||
});
|
||||
|
||||
export const DynamicSecretVerticaSchema = z.object({
|
||||
host: z.string().trim().toLowerCase(),
|
||||
port: z.number(),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
database: z.string().trim(),
|
||||
gatewayId: z.string().nullable().optional(),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
passwordRequirements: z
|
||||
.object({
|
||||
length: z.number().min(1).max(250),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(0),
|
||||
uppercase: z.number().min(0),
|
||||
digits: z.number().min(0),
|
||||
symbols: z.number().min(0)
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 250;
|
||||
}, "Sum of required characters cannot exceed 250"),
|
||||
allowedSymbols: z.string().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length")
|
||||
.optional()
|
||||
.describe("Password generation requirements")
|
||||
});
|
||||
|
||||
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
|
||||
z.object({
|
||||
configType: z.literal(TotpConfigType.URL),
|
||||
@ -320,7 +370,9 @@ export enum DynamicSecretProviders {
|
||||
SapHana = "sap-hana",
|
||||
Snowflake = "snowflake",
|
||||
Totp = "totp",
|
||||
SapAse = "sap-ase"
|
||||
SapAse = "sap-ase",
|
||||
Kubernetes = "kubernetes",
|
||||
Vertica = "vertica"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@ -338,11 +390,17 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
create: (inputs: unknown, expireAt: number) => Promise<{ entityId: string; data: unknown }>;
|
||||
create: (arg: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
}) => Promise<{ entityId: string; data: unknown }>;
|
||||
validateConnection: (inputs: unknown) => Promise<boolean>;
|
||||
validateProviderInputs: (inputs: object) => Promise<unknown>;
|
||||
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -12,8 +13,14 @@ const generatePassword = (size = 48) => {
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
@ -57,11 +64,12 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
await client({
|
||||
|
@ -1,3 +1,4 @@
|
||||
import handlebars from "handlebars";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@ -12,8 +13,14 @@ const generatePassword = (size = 48) => {
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
@ -53,11 +60,12 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import axios, { Axios } from "axios";
|
||||
import handlebars from "handlebars";
|
||||
import https from "https";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@ -14,8 +15,14 @@ const generatePassword = () => {
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
type TCreateRabbitMQUser = {
|
||||
@ -110,11 +117,12 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
return infoResponse;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
|
||||
await createRabbitMqUser({
|
||||
|
@ -15,8 +15,14 @@ const generatePassword = () => {
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
const executeTransactions = async (connection: Redis, commands: string[]): Promise<(string | null)[] | null> => {
|
||||
@ -115,11 +121,12 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return pingResponse;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
|
@ -15,8 +15,14 @@ const generatePassword = (size = 48) => {
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(25);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = `inf_${alphaNumericNanoId(25)}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
enum SapCommands {
|
||||
@ -81,11 +87,12 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const username = `inf_${generateUsername()}`;
|
||||
const password = `${generatePassword()}`;
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
|
@ -21,8 +21,14 @@ const generatePassword = (size = 48) => {
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
@ -91,10 +97,11 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
return testResult;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
|
@ -17,8 +17,14 @@ const generatePassword = (size = 48) => {
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return `infisical_${alphaNumericNanoId(32)}`; // username must start with alpha character, hence prefix
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = `infisical_${alphaNumericNanoId(32)}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
const getDaysToExpiry = (expiryDate: Date) => {
|
||||
@ -82,12 +88,13 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
return isValidConnection;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword();
|
||||
|
||||
try {
|
||||
|
@ -3,7 +3,7 @@ import handlebars from "handlebars";
|
||||
import knex from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@ -104,11 +104,21 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire
|
||||
}
|
||||
};
|
||||
|
||||
const generateUsername = (provider: SqlProviders) => {
|
||||
// For oracle, the client assumes everything is upper case when not using quotes around the password
|
||||
if (provider === SqlProviders.Oracle) return alphaNumericNanoId(32).toUpperCase();
|
||||
const generateUsername = (provider: SqlProviders, usernameTemplate?: string | null) => {
|
||||
let randomUsername = "";
|
||||
|
||||
return alphaNumericNanoId(32);
|
||||
// For oracle, the client assumes everything is upper case when not using quotes around the password
|
||||
if (provider === SqlProviders.Oracle) {
|
||||
randomUsername = alphaNumericNanoId(32).toUpperCase();
|
||||
} else {
|
||||
randomUsername = alphaNumericNanoId(32);
|
||||
}
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
type TSqlDatabaseProviderDTO = {
|
||||
@ -175,6 +185,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
@ -210,9 +221,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const username = generateUsername(providerInputs.client, usernameTemplate);
|
||||
|
||||
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
|
368
backend/src/ee/services/dynamic-secret/providers/vertica.ts
Normal file
368
backend/src/ee/services/dynamic-secret/providers/vertica.ts
Normal file
@ -0,0 +1,368 @@
|
||||
import { randomInt } from "crypto";
|
||||
import handlebars from "handlebars";
|
||||
import knex, { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretVerticaSchema, PasswordRequirements, TDynamicProviderFns } from "./models";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
interface VersionResult {
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface SessionResult {
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
interface DatabaseQueryResult {
|
||||
rows?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
// Extended Knex client interface to handle Vertica-specific overrides
|
||||
interface VerticaKnexClient extends Knex {
|
||||
client: {
|
||||
parseVersion?: () => string;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
const generatePassword = (requirements?: PasswordRequirements) => {
|
||||
const finalReqs = requirements || DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
try {
|
||||
const { length, required, allowedSymbols } = finalReqs;
|
||||
|
||||
const chars = {
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
digits: "0123456789",
|
||||
symbols: allowedSymbols || "-_.~!*"
|
||||
};
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (required.lowercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.lowercase)
|
||||
.fill(0)
|
||||
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.uppercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.uppercase)
|
||||
.fill(0)
|
||||
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.digits > 0) {
|
||||
parts.push(
|
||||
...Array(required.digits)
|
||||
.fill(0)
|
||||
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.symbols > 0) {
|
||||
parts.push(
|
||||
...Array(required.symbols)
|
||||
.fill(0)
|
||||
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||
);
|
||||
}
|
||||
|
||||
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||
|
||||
const allowedChars = Object.entries(chars)
|
||||
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||
.map(([, value]) => value)
|
||||
.join("");
|
||||
|
||||
parts.push(
|
||||
...Array(remainingLength)
|
||||
.fill(0)
|
||||
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||
);
|
||||
|
||||
// shuffle the array to mix up the characters
|
||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||
const j = randomInt(i + 1);
|
||||
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`Failed to generate password: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const randomUsername = `inf_${alphaNumericNanoId(25)}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
});
|
||||
};
|
||||
|
||||
type TVerticaProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
};
|
||||
|
||||
export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretVerticaSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
|
||||
validateHandlebarTemplate("Vertica creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password"].includes(val)
|
||||
});
|
||||
if (providerInputs.revocationStatement) {
|
||||
validateHandlebarTemplate("Vertica revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
}
|
||||
return { ...providerInputs, hostIp };
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretVerticaSchema> & { hostIp: string }) => {
|
||||
const config = {
|
||||
client: "pg",
|
||||
connection: {
|
||||
host: providerInputs.hostIp,
|
||||
port: providerInputs.port,
|
||||
database: providerInputs.database,
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
ssl: false
|
||||
},
|
||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
pool: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
acquireTimeoutMillis: 30000,
|
||||
createTimeoutMillis: 30000,
|
||||
destroyTimeoutMillis: 5000,
|
||||
idleTimeoutMillis: 30000,
|
||||
reapIntervalMillis: 1000,
|
||||
createRetryIntervalMillis: 100
|
||||
},
|
||||
// Disable version checking for Vertica compatibility
|
||||
version: "9.6.0" // Fake a compatible PostgreSQL version
|
||||
};
|
||||
|
||||
const client = knex(config) as VerticaKnexClient;
|
||||
|
||||
// Override the version parsing to prevent errors with Vertica
|
||||
if (client.client && typeof client.client.parseVersion !== "undefined") {
|
||||
client.client.parseVersion = () => "9.6.0";
|
||||
}
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const gatewayProxyWrapper = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretVerticaSchema>,
|
||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||
) => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
await withGatewayProxy(
|
||||
async (port) => {
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
identityId: relayDetails.identityId,
|
||||
orgId: relayDetails.orgId,
|
||||
tlsOptions: {
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey.toString()
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
let isConnected = false;
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => {
|
||||
let client: VerticaKnexClient | null = null;
|
||||
|
||||
try {
|
||||
client = await $getClient({ ...providerInputs, hostIp: host, port });
|
||||
|
||||
const clientResult: DatabaseQueryResult = await client.raw("SELECT version() AS version");
|
||||
|
||||
const resultFromSelectedDatabase = clientResult.rows?.[0] as VersionResult | undefined;
|
||||
|
||||
if (!resultFromSelectedDatabase?.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate Vertica connection, version query failed"
|
||||
});
|
||||
}
|
||||
|
||||
isConnected = true;
|
||||
} finally {
|
||||
if (client) await client.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const password = generatePassword(providerInputs.passwordRequirements);
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
let client: VerticaKnexClient | null = null;
|
||||
|
||||
try {
|
||||
client = await $getClient({ ...providerInputs, hostIp: host, port });
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
// Execute queries sequentially to maintain transaction integrity
|
||||
for (const query of queries) {
|
||||
const trimmedQuery = query.trim();
|
||||
if (trimmedQuery) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await client.raw(trimmedQuery);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (client) await client.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, username: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
let client: VerticaKnexClient | null = null;
|
||||
|
||||
try {
|
||||
client = await $getClient({ ...providerInputs, hostIp: host, port });
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({
|
||||
username
|
||||
});
|
||||
|
||||
const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
// Check for active sessions and close them
|
||||
try {
|
||||
const sessionResult: DatabaseQueryResult = await client.raw(
|
||||
"SELECT session_id FROM sessions WHERE user_name = ?",
|
||||
[username]
|
||||
);
|
||||
|
||||
const activeSessions = (sessionResult.rows || []) as SessionResult[];
|
||||
|
||||
// Close all sessions in parallel since they're independent operations
|
||||
if (activeSessions.length > 0) {
|
||||
const sessionClosePromises = activeSessions.map(async (session) => {
|
||||
try {
|
||||
await client!.raw("SELECT close_session(?)", [session.session_id]);
|
||||
} catch (error) {
|
||||
// Continue if session is already closed
|
||||
logger.error(error, `Failed to close session ${session.session_id}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(sessionClosePromises);
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue if we can't query sessions (permissions, etc.)
|
||||
logger.error(error, "Could not query/close active sessions");
|
||||
}
|
||||
|
||||
// Execute revocation queries sequentially to maintain transaction integrity
|
||||
for (const query of queries) {
|
||||
const trimmedQuery = query.trim();
|
||||
if (trimmedQuery) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await client.raw(trimmedQuery);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (client) await client.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (_: unknown, username: string) => {
|
||||
// No need for renewal
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user