mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 17:28:18 +00:00
Merge remote-tracking branch 'origin/main' into feature/oidc
This commit is contained in:
@@ -35,7 +35,7 @@ jobs:
|
|||||||
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||||
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
||||||
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
||||||
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
|
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
|
||||||
env:
|
env:
|
||||||
REDIS_URL: redis://172.17.0.1:6379
|
REDIS_URL: redis://172.17.0.1:6379
|
||||||
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
||||||
|
|||||||
Generated
+205
-10
@@ -25,6 +25,8 @@
|
|||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/x509": "^1.10.0",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
@@ -2461,9 +2463,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fastify/session": {
|
"node_modules/@fastify/session": {
|
||||||
"version": "10.7.0",
|
"version": "10.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.9.0.tgz",
|
||||||
"integrity": "sha512-ECA75gnyaxcyIukgyO2NGT3XdbLReNl/pTKrrkRfDc6pVqNtdptwwfx9KXrIMOfsO4B3m84eF3wZ9GgnebiZ4w==",
|
"integrity": "sha512-u/c42RuAaxCeEuRCAwK2+/SfGqKOd0NSyRzEvDwFBWySQoKUZQyb9OmmJSWJBbOP1OfaU2OsDrjbPbghE1l/YQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fastify-plugin": "^4.0.0",
|
"fastify-plugin": "^4.0.0",
|
||||||
"safe-stable-stringify": "^2.3.1"
|
"safe-stable-stringify": "^2.3.1"
|
||||||
@@ -3301,6 +3303,149 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
|
||||||
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
|
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@peculiar/asn1-cms": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-Wtk9R7yQxGaIaawHorWKP2OOOm/RZzamOmSWwaqGphIuU6TcKYih0slL6asZlSSZtVoYTrBfrddSOD/jTu9vuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509-attr": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-csr": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-ZmAaP2hfzgIGdMLcot8gHTykzoI+X/S53x1xoGbTmratETIaAbSWMiPGvZmXRA0SNEIydpMkzYtq4fQBxN1u1w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-ecc": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-pfx": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-XhdnCVznMmSmgy68B9pVxiZ1XkKoE1BjO4Hv+eUGiY1pM14msLsFZ3N7K46SoITIVZLq92kKkXpGiTfRjlNLyg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-cms": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pkcs8": "^2.3.8",
|
||||||
|
"@peculiar/asn1-rsa": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-pkcs8": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-rL8k2x59v8lZiwLRqdMMmOJ30GHt6yuHISFIuuWivWjAJjnxzZBVzMTQ72sknX5MeTSSvGwPmEFk2/N8+UztFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-pkcs9": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-+nONq5tcK7vm3qdY7ZKoSQGQjhJYMJbwJGbXLFOhmqsFIxEWyQPHyV99+wshOjpOjg0wUSSkEEzX2hx5P6EKeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-cms": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pfx": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pkcs8": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509-attr": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-rsa": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-schema": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==",
|
||||||
|
"dependencies": {
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-x509": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"ipaddr.js": "^2.1.0",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-x509-attr": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-4Z8mSN95MOuX04Aku9BUyMdsMKtVQUqWnr627IheiWnwFoheUhX3R4Y2zh23M7m80r4/WG8MOAckRKc77IRv6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/x509": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-cms": "^2.3.8",
|
||||||
|
"@peculiar/asn1-csr": "^2.3.8",
|
||||||
|
"@peculiar/asn1-ecc": "^2.3.8",
|
||||||
|
"@peculiar/asn1-pkcs9": "^2.3.8",
|
||||||
|
"@peculiar/asn1-rsa": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"tsyringe": "^4.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@phc/format": {
|
"node_modules/@phc/format": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||||
@@ -5956,6 +6101,19 @@
|
|||||||
"safer-buffer": "~2.1.0"
|
"safer-buffer": "~2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1js": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"pvtsutils": "^1.3.2",
|
||||||
|
"pvutils": "^1.1.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assert-plus": {
|
"node_modules/assert-plus": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
@@ -6303,12 +6461,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -8019,9 +8177,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
@@ -11826,6 +11984,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pvtsutils": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pvutils": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.11.0",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
@@ -12016,6 +12190,11 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reflect-metadata": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||||
@@ -13799,6 +13978,22 @@
|
|||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tsyringe": {
|
||||||
|
"version": "4.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz",
|
||||||
|
"integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsyringe/node_modules/tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
|
},
|
||||||
"node_modules/tweetnacl": {
|
"node_modules/tweetnacl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||||
|
|||||||
@@ -86,6 +86,8 @@
|
|||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/x509": "^1.10.0",
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
|
|||||||
Vendored
+7
-1
@@ -6,6 +6,7 @@ import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-ap
|
|||||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
|
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
@@ -15,6 +16,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||||
|
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
@@ -30,6 +32,8 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
|||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
|
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
@@ -49,7 +53,6 @@ import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env
|
|||||||
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
import { TRateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service";
|
|
||||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
||||||
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
@@ -140,6 +143,9 @@ declare module "fastify" {
|
|||||||
ldap: TLdapConfigServiceFactory;
|
ldap: TLdapConfigServiceFactory;
|
||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
|
certificate: TCertificateServiceFactory;
|
||||||
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
trustedIp: TTrustedIpServiceFactory;
|
trustedIp: TTrustedIpServiceFactory;
|
||||||
|
|||||||
Vendored
+52
@@ -32,6 +32,27 @@ import {
|
|||||||
TBackupPrivateKey,
|
TBackupPrivateKey,
|
||||||
TBackupPrivateKeyInsert,
|
TBackupPrivateKeyInsert,
|
||||||
TBackupPrivateKeyUpdate,
|
TBackupPrivateKeyUpdate,
|
||||||
|
TCertificateAuthorities,
|
||||||
|
TCertificateAuthoritiesInsert,
|
||||||
|
TCertificateAuthoritiesUpdate,
|
||||||
|
TCertificateAuthorityCerts,
|
||||||
|
TCertificateAuthorityCertsInsert,
|
||||||
|
TCertificateAuthorityCertsUpdate,
|
||||||
|
TCertificateAuthorityCrl,
|
||||||
|
TCertificateAuthorityCrlInsert,
|
||||||
|
TCertificateAuthorityCrlUpdate,
|
||||||
|
TCertificateAuthoritySecret,
|
||||||
|
TCertificateAuthoritySecretInsert,
|
||||||
|
TCertificateAuthoritySecretUpdate,
|
||||||
|
TCertificateBodies,
|
||||||
|
TCertificateBodiesInsert,
|
||||||
|
TCertificateBodiesUpdate,
|
||||||
|
TCertificates,
|
||||||
|
TCertificateSecrets,
|
||||||
|
TCertificateSecretsInsert,
|
||||||
|
TCertificateSecretsUpdate,
|
||||||
|
TCertificatesInsert,
|
||||||
|
TCertificatesUpdate,
|
||||||
TDynamicSecretLeases,
|
TDynamicSecretLeases,
|
||||||
TDynamicSecretLeasesInsert,
|
TDynamicSecretLeasesInsert,
|
||||||
TDynamicSecretLeasesUpdate,
|
TDynamicSecretLeasesUpdate,
|
||||||
@@ -263,6 +284,37 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.CertificateAuthority]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthorities,
|
||||||
|
TCertificateAuthoritiesInsert,
|
||||||
|
TCertificateAuthoritiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateAuthorityCert]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthorityCerts,
|
||||||
|
TCertificateAuthorityCertsInsert,
|
||||||
|
TCertificateAuthorityCertsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateAuthoritySecret]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthoritySecret,
|
||||||
|
TCertificateAuthoritySecretInsert,
|
||||||
|
TCertificateAuthoritySecretUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateAuthorityCrl]: Knex.CompositeTableType<
|
||||||
|
TCertificateAuthorityCrl,
|
||||||
|
TCertificateAuthorityCrlInsert,
|
||||||
|
TCertificateAuthorityCrlUpdate
|
||||||
|
>;
|
||||||
|
[TableName.Certificate]: Knex.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||||
|
[TableName.CertificateBody]: Knex.CompositeTableType<
|
||||||
|
TCertificateBodies,
|
||||||
|
TCertificateBodiesInsert,
|
||||||
|
TCertificateBodiesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.CertificateSecret]: Knex.CompositeTableType<
|
||||||
|
TCertificateSecrets,
|
||||||
|
TCertificateSecretsInsert,
|
||||||
|
TCertificateSecretsUpdate
|
||||||
|
>;
|
||||||
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
[TableName.UserGroupMembership]: Knex.CompositeTableType<
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUserGroupMembershipInsert,
|
TUserGroupMembershipInsert,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType");
|
||||||
|
await knex.schema.alterTable(TableName.SecretTag, (tb) => {
|
||||||
|
if (!hasCreatedByActorType) {
|
||||||
|
tb.string("createdByActorType").notNullable().defaultTo(ActorType.USER);
|
||||||
|
tb.dropForeign("createdBy");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType");
|
||||||
|
await knex.schema.alterTable(TableName.SecretTag, (tb) => {
|
||||||
|
if (hasCreatedByActorType) {
|
||||||
|
tb.dropColumn("createdByActorType");
|
||||||
|
tb.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
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.Project)) {
|
||||||
|
const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId");
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
if (!doesProjectCertificateKeyIdExist) {
|
||||||
|
t.uuid("kmsCertificateKeyId").nullable();
|
||||||
|
t.foreign("kmsCertificateKeyId").references("id").inTable(TableName.KmsKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthority))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthority, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("parentCaId").nullable();
|
||||||
|
t.foreign("parentCaId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.string("type").notNullable(); // root / intermediate
|
||||||
|
t.string("status").notNullable(); // active / pending-certificate
|
||||||
|
t.string("friendlyName").notNullable();
|
||||||
|
t.string("organization").notNullable();
|
||||||
|
t.string("ou").notNullable();
|
||||||
|
t.string("country").notNullable();
|
||||||
|
t.string("province").notNullable();
|
||||||
|
t.string("locality").notNullable();
|
||||||
|
t.string("commonName").notNullable();
|
||||||
|
t.string("dn").notNullable();
|
||||||
|
t.string("serialNumber").nullable().unique();
|
||||||
|
t.integer("maxPathLength").nullable();
|
||||||
|
t.string("keyAlgorithm").notNullable();
|
||||||
|
t.datetime("notBefore").nullable();
|
||||||
|
t.datetime("notAfter").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCert))) {
|
||||||
|
// table to keep track of certificates belonging to CA
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable().unique();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCertificate").notNullable();
|
||||||
|
t.binary("encryptedCertificateChain").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthoritySecret))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthoritySecret, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable().unique();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedPrivateKey").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCrl))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateAuthorityCrl, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable().unique();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCrl").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.Certificate))) {
|
||||||
|
await knex.schema.createTable(TableName.Certificate, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("caId").notNullable();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.string("status").notNullable(); // active / pending-certificate
|
||||||
|
t.string("serialNumber").notNullable().unique();
|
||||||
|
t.string("friendlyName").notNullable();
|
||||||
|
t.string("commonName").notNullable();
|
||||||
|
t.datetime("notBefore").notNullable();
|
||||||
|
t.datetime("notAfter").notNullable();
|
||||||
|
t.datetime("revokedAt").nullable();
|
||||||
|
t.integer("revocationReason").nullable(); // integer based on crl reason in RFC 5280
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateBody))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateBody, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("certId").notNullable().unique();
|
||||||
|
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||||
|
t.binary("encryptedCertificate").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateAuthority);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.Certificate);
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
// project
|
||||||
|
if (await knex.schema.hasTable(TableName.Project)) {
|
||||||
|
const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId");
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
if (doesProjectCertificateKeyIdExist) t.dropColumn("kmsCertificateKeyId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificates
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateBody);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateBody);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.Certificate);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.Certificate);
|
||||||
|
|
||||||
|
// certificate authorities
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthoritySecret);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCrl);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCrl);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCert);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateAuthority);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateAuthority);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
|
||||||
|
const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
if (hasOrgIdColumn) t.uuid("orgId").nullable().alter();
|
||||||
|
if (hasUserIdColumn) t.uuid("userId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
|
||||||
|
const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
if (hasOrgIdColumn) t.uuid("orgId").notNullable().alter();
|
||||||
|
if (hasUserIdColumn) t.uuid("userId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 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(),
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||||
|
export type TCertificateAuthoritiesInsert = Omit<z.input<typeof CertificateAuthoritiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateAuthoritiesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthoritiesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// 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 CertificateAuthorityCertsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
encryptedCertificate: zodBuffer,
|
||||||
|
encryptedCertificateChain: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>;
|
||||||
|
export type TCertificateAuthorityCertsInsert = Omit<z.input<typeof CertificateAuthorityCertsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateAuthorityCertsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthorityCertsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -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 { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateAuthorityCrlSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
encryptedCrl: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;
|
||||||
|
export type TCertificateAuthorityCrlInsert = Omit<z.input<typeof CertificateAuthorityCrlSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateAuthorityCrlUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthorityCrlSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateAuthoritySecretSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
encryptedPrivateKey: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateAuthoritySecret = z.infer<typeof CertificateAuthoritySecretSchema>;
|
||||||
|
export type TCertificateAuthoritySecretInsert = Omit<
|
||||||
|
z.input<typeof CertificateAuthoritySecretSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TCertificateAuthoritySecretUpdate = Partial<
|
||||||
|
Omit<z.input<typeof CertificateAuthoritySecretSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateBodiesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
certId: z.string().uuid(),
|
||||||
|
encryptedCertificate: zodBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateBodies = z.infer<typeof CertificateBodiesSchema>;
|
||||||
|
export type TCertificateBodiesInsert = Omit<z.input<typeof CertificateBodiesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateBodiesUpdate = Partial<Omit<z.input<typeof CertificateBodiesSchema>, TImmutableDBKeys>>;
|
||||||
@@ -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 CertificateSecretsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
certId: z.string().uuid(),
|
||||||
|
pk: z.string(),
|
||||||
|
sk: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateSecrets = z.infer<typeof CertificateSecretsSchema>;
|
||||||
|
export type TCertificateSecretsInsert = Omit<z.input<typeof CertificateSecretsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateSecretsUpdate = Partial<Omit<z.input<typeof CertificateSecretsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificatesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
serialNumber: z.string(),
|
||||||
|
friendlyName: z.string(),
|
||||||
|
commonName: z.string(),
|
||||||
|
notBefore: z.date(),
|
||||||
|
notAfter: z.date(),
|
||||||
|
revokedAt: z.date().nullable().optional(),
|
||||||
|
revocationReason: z.number().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
export type TCertificatesInsert = Omit<z.input<typeof CertificatesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificatesUpdate = Partial<Omit<z.input<typeof CertificatesSchema>, TImmutableDBKeys>>;
|
||||||
@@ -8,6 +8,13 @@ export * from "./audit-logs";
|
|||||||
export * from "./auth-token-sessions";
|
export * from "./auth-token-sessions";
|
||||||
export * from "./auth-tokens";
|
export * from "./auth-tokens";
|
||||||
export * from "./backup-private-key";
|
export * from "./backup-private-key";
|
||||||
|
export * from "./certificate-authorities";
|
||||||
|
export * from "./certificate-authority-certs";
|
||||||
|
export * from "./certificate-authority-crl";
|
||||||
|
export * from "./certificate-authority-secret";
|
||||||
|
export * from "./certificate-bodies";
|
||||||
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
CertificateAuthority = "certificate_authorities",
|
||||||
|
CertificateAuthorityCert = "certificate_authority_certs",
|
||||||
|
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||||
|
CertificateAuthorityCrl = "certificate_authority_crl",
|
||||||
|
Certificate = "certificates",
|
||||||
|
CertificateBody = "certificate_bodies",
|
||||||
|
CertificateSecret = "certificate_secrets",
|
||||||
Groups = "groups",
|
Groups = "groups",
|
||||||
GroupProjectMembership = "group_project_memberships",
|
GroupProjectMembership = "group_project_memberships",
|
||||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const ProjectsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
version: z.number().default(1),
|
version: z.number().default(1),
|
||||||
upgradeStatus: z.string().nullable().optional(),
|
upgradeStatus: z.string().nullable().optional(),
|
||||||
|
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||||
pitVersionLimit: z.number().default(10)
|
pitVersionLimit: z.number().default(10)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export const SecretSharingSchema = z.object({
|
|||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
expiresAfterViews: z.number().nullable().optional()
|
expiresAfterViews: z.number().nullable().optional()
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const SecretTagsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
createdBy: z.string().uuid().nullable().optional(),
|
createdBy: z.string().uuid().nullable().optional(),
|
||||||
projectId: z.string()
|
projectId: z.string(),
|
||||||
|
createdByActorType: z.string().default("user")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretTags = z.infer<typeof SecretTagsSchema>;
|
export type TSecretTags = z.infer<typeof SecretTagsSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId/crl",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get CRL of the CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CA_CRL,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
crl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// server.route({
|
||||||
|
// method: "GET",
|
||||||
|
// url: "/:caId/crl/rotate",
|
||||||
|
// config: {
|
||||||
|
// rateLimit: writeLimit
|
||||||
|
// },
|
||||||
|
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
// schema: {
|
||||||
|
// description: "Rotate CRL of the CA",
|
||||||
|
// params: z.object({
|
||||||
|
// caId: z.string().trim()
|
||||||
|
// }),
|
||||||
|
// response: {
|
||||||
|
// 200: z.object({
|
||||||
|
// message: z.string()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// handler: async (req) => {
|
||||||
|
// await server.services.certificateAuthority.rotateCaCrl({
|
||||||
|
// caId: req.params.caId,
|
||||||
|
// actor: req.permission.type,
|
||||||
|
// actorId: req.permission.id,
|
||||||
|
// actorAuthMethod: req.permission.authMethod,
|
||||||
|
// actorOrgId: req.permission.orgId
|
||||||
|
// });
|
||||||
|
// return {
|
||||||
|
// message: "Successfully rotated CA CRL"
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||||
|
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
@@ -11,6 +12,7 @@ import { registerOidcRouter } from "./oidc-router";
|
|||||||
import { registerOrgRoleRouter } from "./org-role-router";
|
import { registerOrgRoleRouter } from "./org-role-router";
|
||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
|
import { registerRateLimitRouter } from "./rate-limit-router";
|
||||||
import { registerSamlRouter } from "./saml-router";
|
import { registerSamlRouter } from "./saml-router";
|
||||||
import { registerScimRouter } from "./scim-router";
|
import { registerScimRouter } from "./scim-router";
|
||||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||||
@@ -46,6 +48,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
||||||
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
||||||
|
await server.register(registerRateLimitRouter, { prefix: "/rate-limit" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (dynamicSecretRouter) => {
|
async (dynamicSecretRouter) => {
|
||||||
@@ -55,6 +58,13 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/dynamic-secrets" }
|
{ prefix: "/dynamic-secrets" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (pkiRouter) => {
|
||||||
|
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
|
||||||
|
},
|
||||||
|
{ prefix: "/pki" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (ssoRouter) => {
|
async (ssoRouter) => {
|
||||||
await ssoRouter.register(registerSamlRouter);
|
await ssoRouter.register(registerSamlRouter);
|
||||||
@@ -62,6 +72,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
{ prefix: "/sso" }
|
{ prefix: "/sso" }
|
||||||
);
|
);
|
||||||
|
|
||||||
await server.register(registerScimRouter, { prefix: "/scim" });
|
await server.register(registerScimRouter, { prefix: "/scim" });
|
||||||
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
await server.register(registerLdapRouter, { prefix: "/ldap" });
|
||||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.workspaceId,
|
||||||
...req.query,
|
...req.query,
|
||||||
startDate: req.query.endDate || getLastMidnightDateISO(),
|
endDate: req.query.endDate,
|
||||||
|
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||||
auditLogActor: req.query.actor,
|
auditLogActor: req.query.actor,
|
||||||
actor: req.permission.type
|
actor: req.permission.type
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
@@ -104,7 +105,21 @@ export enum EventType {
|
|||||||
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
|
SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
|
||||||
|
CREATE_CA = "create-certificate-authority",
|
||||||
|
GET_CA = "get-certificate-authority",
|
||||||
|
UPDATE_CA = "update-certificate-authority",
|
||||||
|
DELETE_CA = "delete-certificate-authority",
|
||||||
|
GET_CA_CSR = "get-certificate-authority-csr",
|
||||||
|
GET_CA_CERT = "get-certificate-authority-cert",
|
||||||
|
SIGN_INTERMEDIATE = "sign-intermediate",
|
||||||
|
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||||
|
GET_CA_CRL = "get-certificate-authority-crl",
|
||||||
|
ISSUE_CERT = "issue-cert",
|
||||||
|
GET_CERT = "get-cert",
|
||||||
|
DELETE_CERT = "delete-cert",
|
||||||
|
REVOKE_CERT = "revoke-cert",
|
||||||
|
GET_CERT_BODY = "get-cert-body"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@@ -843,6 +858,125 @@ interface SecretApprovalRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateCa {
|
||||||
|
type: EventType.CREATE_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCa {
|
||||||
|
type: EventType.GET_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateCa {
|
||||||
|
type: EventType.UPDATE_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
status: CaStatus;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCa {
|
||||||
|
type: EventType.DELETE_CA;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCaCsr {
|
||||||
|
type: EventType.GET_CA_CSR;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCaCert {
|
||||||
|
type: EventType.GET_CA_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignIntermediate {
|
||||||
|
type: EventType.SIGN_INTERMEDIATE;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportCaCert {
|
||||||
|
type: EventType.IMPORT_CA_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCaCrl {
|
||||||
|
type: EventType.GET_CA_CRL;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueCert {
|
||||||
|
type: EventType.ISSUE_CERT;
|
||||||
|
metadata: {
|
||||||
|
caId: string;
|
||||||
|
dn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCert {
|
||||||
|
type: EventType.GET_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCert {
|
||||||
|
type: EventType.DELETE_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RevokeCert {
|
||||||
|
type: EventType.REVOKE_CERT;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCertBody {
|
||||||
|
type: EventType.GET_CERT_BODY;
|
||||||
|
metadata: {
|
||||||
|
certId: string;
|
||||||
|
cn: string;
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -910,4 +1044,18 @@ export type Event =
|
|||||||
| SecretApprovalMerge
|
| SecretApprovalMerge
|
||||||
| SecretApprovalClosed
|
| SecretApprovalClosed
|
||||||
| SecretApprovalRequest
|
| SecretApprovalRequest
|
||||||
| SecretApprovalReopened;
|
| SecretApprovalReopened
|
||||||
|
| CreateCa
|
||||||
|
| GetCa
|
||||||
|
| UpdateCa
|
||||||
|
| DeleteCa
|
||||||
|
| GetCaCsr
|
||||||
|
| GetCaCert
|
||||||
|
| SignIntermediate
|
||||||
|
| ImportCaCert
|
||||||
|
| GetCaCrl
|
||||||
|
| IssueCert
|
||||||
|
| GetCert
|
||||||
|
| DeleteCert
|
||||||
|
| RevokeCert
|
||||||
|
| GetCertBody;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCrlDALFactory = ReturnType<typeof certificateAuthorityCrlDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityCrlDALFactory = (db: TDbClient) => {
|
||||||
|
const caCrlOrm = ormify(db, TableName.CertificateAuthorityCrl);
|
||||||
|
return caCrlOrm;
|
||||||
|
};
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { TGetCrl } from "./certificate-authority-crl-types";
|
||||||
|
|
||||||
|
type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityCrlServiceFactory = ({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
}: TCertificateAuthorityCrlServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Return the Certificate Revocation List (CRL) for CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.caCrl)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id });
|
||||||
|
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCrl = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCrl.encryptedCrl
|
||||||
|
});
|
||||||
|
|
||||||
|
const crl = new x509.X509Crl(decryptedCrl);
|
||||||
|
|
||||||
|
const base64crl = crl.toString("base64");
|
||||||
|
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
crl: crlPem,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// const rotateCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TRotateCrlDTO) => {
|
||||||
|
// const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
// if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
// const { permission } = await permissionService.getProjectPermission(
|
||||||
|
// actor,
|
||||||
|
// actorId,
|
||||||
|
// ca.projectId,
|
||||||
|
// actorAuthMethod,
|
||||||
|
// actorOrgId
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
// ProjectPermissionActions.Read,
|
||||||
|
// ProjectPermissionSub.CertificateAuthorities
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
// const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
// const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
// projectId: ca.projectId,
|
||||||
|
// projectDAL,
|
||||||
|
// kmsService
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const privateKey = await kmsService.decrypt({
|
||||||
|
// kmsId: keyId,
|
||||||
|
// cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
||||||
|
// const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||||
|
// "sign"
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// const revokedCerts = await certificateDAL.find({
|
||||||
|
// caId: ca.id,
|
||||||
|
// status: CertStatus.REVOKED
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const crl = await x509.X509CrlGenerator.create({
|
||||||
|
// issuer: ca.dn,
|
||||||
|
// thisUpdate: new Date(),
|
||||||
|
// nextUpdate: new Date("2025/12/12"),
|
||||||
|
// entries: revokedCerts.map((revokedCert) => {
|
||||||
|
// return {
|
||||||
|
// serialNumber: revokedCert.serialNumber,
|
||||||
|
// revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||||
|
// reason: revokedCert.revocationReason as number,
|
||||||
|
// invalidity: new Date("2022/01/01"),
|
||||||
|
// issuer: ca.dn
|
||||||
|
// };
|
||||||
|
// }),
|
||||||
|
// signingAlgorithm: alg,
|
||||||
|
// signingKey: sk
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
// kmsId: keyId,
|
||||||
|
// plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await certificateAuthorityCrlDAL.update(
|
||||||
|
// {
|
||||||
|
// caId: ca.id
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// encryptedCrl
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const base64crl = crl.toString("base64");
|
||||||
|
// const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// crl: crlPem
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCaCrl
|
||||||
|
// rotateCaCrl
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TGetCrl = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -25,6 +25,7 @@ export const getDefaultOnPremFeatures = () => {
|
|||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: false,
|
||||||
secretRotation: true
|
secretRotation: true,
|
||||||
|
caCrl: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: false,
|
||||||
secretRotation: true
|
secretRotation: true,
|
||||||
|
caCrl: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
|||||||
@@ -575,6 +575,9 @@ export const licenseServiceFactory = ({
|
|||||||
getInstanceType() {
|
getInstanceType() {
|
||||||
return instanceType;
|
return instanceType;
|
||||||
},
|
},
|
||||||
|
get onPremFeatures() {
|
||||||
|
return onPremFeatures;
|
||||||
|
},
|
||||||
getPlan,
|
getPlan,
|
||||||
updateSubscriptionOrgMemberCount,
|
updateSubscriptionOrgMemberCount,
|
||||||
refreshPlan,
|
refreshPlan,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type TFeatureSet = {
|
|||||||
has_used_trial: true;
|
has_used_trial: true;
|
||||||
secretApproval: false;
|
secretApproval: false;
|
||||||
secretRotation: true;
|
secretRotation: true;
|
||||||
|
caCrl: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgPlansTableDTO = {
|
export type TOrgPlansTableDTO = {
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ export enum ProjectPermissionSub {
|
|||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
Identity = "identity"
|
Identity = "identity",
|
||||||
|
CertificateAuthorities = "certificate-authorities",
|
||||||
|
Certificates = "certificates"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
@@ -53,6 +55,8 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
@@ -139,6 +143,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
@@ -205,6 +220,14 @@ const buildMemberPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,6 +252,8 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-2
@@ -2,6 +2,7 @@ import { CronJob } from "cron";
|
|||||||
|
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TRateLimitDALFactory } from "./rate-limit-dal";
|
import { TRateLimitDALFactory } from "./rate-limit-dal";
|
||||||
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
|
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
|
||||||
|
|
||||||
@@ -24,11 +25,12 @@ export const getRateLimiterConfig = () => {
|
|||||||
|
|
||||||
type TRateLimitServiceFactoryDep = {
|
type TRateLimitServiceFactoryDep = {
|
||||||
rateLimitDAL: TRateLimitDALFactory;
|
rateLimitDAL: TRateLimitDALFactory;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TRateLimitServiceFactory = ReturnType<typeof rateLimitServiceFactory>;
|
export type TRateLimitServiceFactory = ReturnType<typeof rateLimitServiceFactory>;
|
||||||
|
|
||||||
export const rateLimitServiceFactory = ({ rateLimitDAL }: TRateLimitServiceFactoryDep) => {
|
export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateLimitServiceFactoryDep) => {
|
||||||
const DEFAULT_RATE_LIMIT_CONFIG_ID = "00000000-0000-0000-0000-000000000000";
|
const DEFAULT_RATE_LIMIT_CONFIG_ID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
const getRateLimits = async (): Promise<TRateLimit | undefined> => {
|
const getRateLimits = async (): Promise<TRateLimit | undefined> => {
|
||||||
@@ -78,7 +80,16 @@ export const rateLimitServiceFactory = ({ rateLimitDAL }: TRateLimitServiceFacto
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeBackgroundSync = () => {
|
const initializeBackgroundSync = async () => {
|
||||||
|
if (!licenseService.onPremFeatures.customRateLimits) {
|
||||||
|
logger.info("Current license does not support custom rate limit configuration");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Setting up background sync process for rate limits");
|
||||||
|
// initial sync upon startup
|
||||||
|
await syncRateLimitConfiguration();
|
||||||
|
|
||||||
// sync rate limits configuration every 10 minutes
|
// sync rate limits configuration every 10 minutes
|
||||||
const job = new CronJob("*/10 * * * *", syncRateLimitConfiguration);
|
const job = new CronJob("*/10 * * * *", syncRateLimitConfiguration);
|
||||||
job.start();
|
job.start();
|
||||||
@@ -343,7 +343,8 @@ export const RAW_SECRETS = {
|
|||||||
secretValue: "The value of the secret to create.",
|
secretValue: "The value of the secret to create.",
|
||||||
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
||||||
type: "The type of the secret to create.",
|
type: "The type of the secret to create.",
|
||||||
workspaceId: "The ID of the project to create the secret in."
|
workspaceId: "The ID of the project to create the secret in.",
|
||||||
|
tagIds: "The ID of the tags to be attached to the created secret."
|
||||||
},
|
},
|
||||||
GET: {
|
GET: {
|
||||||
secretName: "The name of the secret to get.",
|
secretName: "The name of the secret to get.",
|
||||||
@@ -364,7 +365,8 @@ export const RAW_SECRETS = {
|
|||||||
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
||||||
type: "The type of the secret to update.",
|
type: "The type of the secret to update.",
|
||||||
projectSlug: "The slug of the project to update the secret in.",
|
projectSlug: "The slug of the project to update the secret in.",
|
||||||
workspaceId: "The ID of the project to update the secret in."
|
workspaceId: "The ID of the project to update the secret in.",
|
||||||
|
tagIds: "The ID of the tags to be attached to the updated secret."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
secretName: "The name of the secret to delete.",
|
secretName: "The name of the secret to delete.",
|
||||||
@@ -506,12 +508,27 @@ export const SECRET_TAGS = {
|
|||||||
LIST: {
|
LIST: {
|
||||||
projectId: "The ID of the project to list tags from."
|
projectId: "The ID of the project to list tags from."
|
||||||
},
|
},
|
||||||
|
GET_TAG_BY_ID: {
|
||||||
|
projectId: "The ID of the project to get tags from.",
|
||||||
|
tagId: "The ID of the tag to get details"
|
||||||
|
},
|
||||||
|
GET_TAG_BY_SLUG: {
|
||||||
|
projectId: "The ID of the project to get tags from.",
|
||||||
|
tagSlug: "The slug of the tag to get details"
|
||||||
|
},
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the tag in.",
|
projectId: "The ID of the project to create the tag in.",
|
||||||
name: "The name of the tag to create.",
|
name: "The name of the tag to create.",
|
||||||
slug: "The slug of the tag to create.",
|
slug: "The slug of the tag to create.",
|
||||||
color: "The color of the tag to create."
|
color: "The color of the tag to create."
|
||||||
},
|
},
|
||||||
|
UPDATE: {
|
||||||
|
projectId: "The ID of the project to update the tag in.",
|
||||||
|
tagId: "The ID of the tag to get details",
|
||||||
|
name: "The name of the tag to update.",
|
||||||
|
slug: "The slug of the tag to update.",
|
||||||
|
color: "The color of the tag to update."
|
||||||
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
tagId: "The ID of the tag to delete.",
|
tagId: "The ID of the tag to delete.",
|
||||||
projectId: "The ID of the project to delete the tag from."
|
projectId: "The ID of the project to delete the tag from."
|
||||||
@@ -728,6 +745,102 @@ export const AUDIT_LOG_STREAMS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CERTIFICATE_AUTHORITIES = {
|
||||||
|
CREATE: {
|
||||||
|
projectSlug: "Slug of the project to create the CA in.",
|
||||||
|
type: "The type of CA to create",
|
||||||
|
friendlyName: "A friendly name for the CA",
|
||||||
|
organization: "The organization (O) for the CA",
|
||||||
|
ou: "The organization unit (OU) for the CA",
|
||||||
|
country: "The country name (C) for the CA",
|
||||||
|
province: "The state of province name for the CA",
|
||||||
|
locality: "The locality name for the CA",
|
||||||
|
commonName: "The common name (CN) for the CA",
|
||||||
|
notBefore: "The date and time when the CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
maxPathLength:
|
||||||
|
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
|
||||||
|
keyAlgorithm:
|
||||||
|
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA."
|
||||||
|
},
|
||||||
|
GET: {
|
||||||
|
caId: "The ID of the CA to get"
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
caId: "The ID of the CA to update",
|
||||||
|
status: "The status of the CA to update to. This can be one of active or disabled"
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
caId: "The ID of the CA to delete"
|
||||||
|
},
|
||||||
|
GET_CSR: {
|
||||||
|
caId: "The ID of the CA to generate CSR from",
|
||||||
|
csr: "The generated CSR from the CA"
|
||||||
|
},
|
||||||
|
GET_CERT: {
|
||||||
|
caId: "The ID of the CA to get the certificate body and certificate chain from",
|
||||||
|
certificate: "The certificate body of the CA",
|
||||||
|
certificateChain: "The certificate chain of the CA",
|
||||||
|
serialNumber: "The serial number of the CA certificate"
|
||||||
|
},
|
||||||
|
SIGN_INTERMEDIATE: {
|
||||||
|
caId: "The ID of the CA to sign the intermediate certificate with",
|
||||||
|
csr: "The CSR to sign with the CA",
|
||||||
|
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
maxPathLength:
|
||||||
|
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
|
||||||
|
certificate: "The signed intermediate certificate",
|
||||||
|
certificateChain: "The certificate chain of the intermediate certificate",
|
||||||
|
issuingCaCertificate: "The certificate of the issuing CA",
|
||||||
|
serialNumber: "The serial number of the intermediate certificate"
|
||||||
|
},
|
||||||
|
IMPORT_CERT: {
|
||||||
|
caId: "The ID of the CA to import the certificate for",
|
||||||
|
certificate: "The certificate body to import",
|
||||||
|
certificateChain: "The certificate chain to import"
|
||||||
|
},
|
||||||
|
ISSUE_CERT: {
|
||||||
|
caId: "The ID of the CA to issue the certificate from",
|
||||||
|
friendlyName: "A friendly name for the certificate",
|
||||||
|
commonName: "The common name (CN) for the certificate",
|
||||||
|
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
|
||||||
|
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||||
|
certificate: "The issued certificate",
|
||||||
|
issuingCaCertificate: "The certificate of the issuing CA",
|
||||||
|
certificateChain: "The certificate chain of the issued certificate",
|
||||||
|
privateKey: "The private key of the issued certificate",
|
||||||
|
serialNumber: "The serial number of the issued certificate"
|
||||||
|
},
|
||||||
|
GET_CRL: {
|
||||||
|
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
||||||
|
crl: "The certificate revocation list (CRL) of the CA"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CERTIFICATES = {
|
||||||
|
GET: {
|
||||||
|
serialNumber: "The serial number of the certificate to get"
|
||||||
|
},
|
||||||
|
REVOKE: {
|
||||||
|
serialNumber:
|
||||||
|
"The serial number of the certificate to revoke. The revoked certificate will be added to the certificate revocation list (CRL) of the CA.",
|
||||||
|
revocationReason: "The reason for revoking the certificate.",
|
||||||
|
revokedAt: "The date and time when the certificate was revoked",
|
||||||
|
serialNumberRes: "The serial number of the revoked certificate."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
serialNumber: "The serial number of the certificate to delete"
|
||||||
|
},
|
||||||
|
GET_CERT: {
|
||||||
|
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for",
|
||||||
|
certificate: "The certificate body of the certificate",
|
||||||
|
certificateChain: "The certificate chain of the certificate",
|
||||||
|
serialNumberRes: "The serial number of the certificate"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const PROJECT_ROLE = {
|
export const PROJECT_ROLE = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectSlug: "Slug of the project to create the role for.",
|
projectSlug: "Slug of the project to create the role for.",
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ export class BadRequestError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
|
||||||
|
super(message ?? "The requested entity is not found");
|
||||||
|
this.name = name || "NotFound";
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DisableRotationErrors extends Error {
|
export class DisableRotationErrors extends Error {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export enum QueueName {
|
|||||||
SecretPushEventScan = "secret-push-event-scan",
|
SecretPushEventScan = "secret-push-event-scan",
|
||||||
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
||||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||||
|
CaCrlRotation = "ca-crl-rotation",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ export enum QueueJobs {
|
|||||||
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
|
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
|
||||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||||
DynamicSecretPruning = "dynamic-secret-pruning",
|
DynamicSecretPruning = "dynamic-secret-pruning",
|
||||||
|
CaCrlRotation = "ca-crl-rotation-job",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
|
||||||
}
|
}
|
||||||
@@ -55,7 +57,6 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
name: QueueJobs.SecretReminder;
|
name: QueueJobs.SecretReminder;
|
||||||
};
|
};
|
||||||
|
|
||||||
[QueueName.SecretRotation]: {
|
[QueueName.SecretRotation]: {
|
||||||
payload: { rotationId: string };
|
payload: { rotationId: string };
|
||||||
name: QueueJobs.SecretRotation;
|
name: QueueJobs.SecretRotation;
|
||||||
@@ -121,6 +122,12 @@ export type TQueueJobTypes = {
|
|||||||
dynamicSecretCfgId: string;
|
dynamicSecretCfgId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
[QueueName.CaCrlRotation]: {
|
||||||
|
name: QueueJobs.CaCrlRotation;
|
||||||
|
payload: {
|
||||||
|
caId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
[QueueName.SecretReplication]: {
|
[QueueName.SecretReplication]: {
|
||||||
name: QueueJobs.SecretReplication;
|
name: QueueJobs.SecretReplication;
|
||||||
payload: TSyncSecretsDTO;
|
payload: TSyncSecretsDTO;
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { Logger } from "pino";
|
|||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
import { rateLimitDALFactory } from "@app/services/rate-limit/rate-limit-dal";
|
|
||||||
import { rateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service";
|
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||||
@@ -71,9 +69,6 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
|
|||||||
|
|
||||||
// Rate limiters and security headers
|
// Rate limiters and security headers
|
||||||
if (appCfg.isProductionMode) {
|
if (appCfg.isProductionMode) {
|
||||||
const rateLimitDAL = rateLimitDALFactory(db);
|
|
||||||
const rateLimitService = rateLimitServiceFactory({ rateLimitDAL });
|
|
||||||
await rateLimitService.syncRateLimitConfiguration();
|
|
||||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
|
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { getRateLimiterConfig } from "@app/services/rate-limit/rate-limit-service";
|
|
||||||
|
|
||||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -70,8 +70,15 @@ export const creationLimit: RateLimitOptions = {
|
|||||||
|
|
||||||
// Public endpoints to avoid brute force attacks
|
// Public endpoints to avoid brute force attacks
|
||||||
export const publicEndpointLimit: RateLimitOptions = {
|
export const publicEndpointLimit: RateLimitOptions = {
|
||||||
// Shared Secrets
|
// Read Shared Secrets
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: () => getRateLimiterConfig().publicEndpointLimit,
|
max: () => getRateLimiterConfig().publicEndpointLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const publicSecretShareCreationLimit: RateLimitOptions = {
|
||||||
|
// Create Shared Secrets
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
max: 5,
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
BadRequestError,
|
BadRequestError,
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
|
NotFoundError,
|
||||||
ScimRequestError,
|
ScimRequestError,
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
@@ -15,6 +16,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name });
|
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name });
|
||||||
|
} else if (error instanceof NotFoundError) {
|
||||||
|
void res.status(404).send({ statusCode: 404, message: error.message, error: error.name });
|
||||||
} else if (error instanceof UnauthorizedError) {
|
} else if (error instanceof UnauthorizedError) {
|
||||||
void res.status(403).send({ statusCode: 403, message: error.message, error: error.name });
|
void res.status(403).send({ statusCode: 403, message: error.message, error: error.name });
|
||||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-lo
|
|||||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
|
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
|
||||||
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
|
import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||||
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
||||||
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
||||||
@@ -36,6 +38,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal
|
|||||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||||
|
import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal";
|
||||||
|
import { rateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
|
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
@@ -74,6 +78,14 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
|||||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
|
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
|
import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
|
import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
|
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||||
|
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
|
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
@@ -124,8 +136,6 @@ import { projectMembershipServiceFactory } from "@app/services/project-membershi
|
|||||||
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
||||||
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||||
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
import { rateLimitDALFactory } from "@app/services/rate-limit/rate-limit-dal";
|
|
||||||
import { rateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service";
|
|
||||||
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
||||||
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
||||||
@@ -452,7 +462,8 @@ export const registerRoutes = async (
|
|||||||
keyStore
|
keyStore
|
||||||
});
|
});
|
||||||
const rateLimitService = rateLimitServiceFactory({
|
const rateLimitService = rateLimitServiceFactory({
|
||||||
rateLimitDAL
|
rateLimitDAL,
|
||||||
|
licenseService
|
||||||
});
|
});
|
||||||
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
|
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
|
||||||
|
|
||||||
@@ -516,6 +527,58 @@ export const registerRoutes = async (
|
|||||||
projectUserMembershipRoleDAL
|
projectUserMembershipRoleDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityDAL = certificateAuthorityDALFactory(db);
|
||||||
|
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
|
||||||
|
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||||
|
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||||
|
|
||||||
|
const certificateDAL = certificateDALFactory(db);
|
||||||
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
|
|
||||||
|
const certificateService = certificateServiceFactory({
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
queueService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthorityQueue,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateAuthorityCrlService = certificateAuthorityCrlServiceFactory({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -532,6 +595,8 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore
|
keyStore
|
||||||
@@ -914,6 +979,9 @@ export const registerRoutes = async (
|
|||||||
ldap: ldapService,
|
ldap: ldapService,
|
||||||
auditLog: auditLogService,
|
auditLog: auditLogService,
|
||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
|
certificate: certificateService,
|
||||||
|
certificateAuthority: certificateAuthorityService,
|
||||||
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
trustedIp: trustedIpService,
|
trustedIp: trustedIpService,
|
||||||
@@ -927,7 +995,10 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
if (appCfg.isProductionMode) {
|
if (appCfg.isProductionMode) {
|
||||||
cronJobs.push(rateLimitService.initializeBackgroundSync());
|
const rateLimitSyncJob = await rateLimitService.initializeBackgroundSync();
|
||||||
|
if (rateLimitSyncJob) {
|
||||||
|
cronJobs.push(rateLimitSyncJob);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
|
|||||||
@@ -0,0 +1,515 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CertificateAuthoritiesSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
|
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
|
|
||||||
|
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Create CA",
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
projectSlug: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.projectSlug),
|
||||||
|
type: z.nativeEnum(CaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type),
|
||||||
|
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||||
|
commonName: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.commonName),
|
||||||
|
organization: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.organization),
|
||||||
|
ou: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.ou),
|
||||||
|
country: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.country),
|
||||||
|
province: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.province),
|
||||||
|
locality: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.locality),
|
||||||
|
// format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
|
||||||
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.CREATE.notBefore),
|
||||||
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.CREATE.notAfter),
|
||||||
|
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.CREATE.maxPathLength),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.nativeEnum(CertKeyAlgorithm)
|
||||||
|
.default(CertKeyAlgorithm.RSA_2048)
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
// Check that at least one of the specified fields is non-empty
|
||||||
|
return [data.commonName, data.organization, data.ou, data.country, data.province, data.locality].some(
|
||||||
|
(field) => field !== ""
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"At least one of the fields commonName, organization, ou, country, province, or locality must be non-empty",
|
||||||
|
path: []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.createCa({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_CA,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.getCaById({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CA,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:caId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.updateCaById({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_CA,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn,
|
||||||
|
status: ca.status as CaStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:caId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Delete CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.DELETE.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
ca: CertificateAuthoritiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const ca = await server.services.certificateAuthority.deleteCaById({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_CA,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId/csr",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get CA CSR",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CSR.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
csr: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CSR.csr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { ca, csr } = await server.services.certificateAuthority.getCaCsr({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CA_CSR,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
csr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:caId/certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get cert and cert chain of a CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate),
|
||||||
|
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain),
|
||||||
|
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, serialNumber, ca } = await server.services.certificateAuthority.getCaCert({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CA_CERT,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/sign-intermediate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Create intermediate CA certificate from parent CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
|
||||||
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
|
||||||
|
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
||||||
|
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificateChain),
|
||||||
|
issuingCaCertificate: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.issuingCaCertificate),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||||
|
await server.services.certificateAuthority.signIntermediate({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SIGN_INTERMEDIATE,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn,
|
||||||
|
serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/import-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Import certificate and chain to CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.caId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificateChain)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string().trim(),
|
||||||
|
caId: z.string().trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { ca } = await server.services.certificateAuthority.importCertToCa({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.IMPORT_CA_CERT,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Successfully imported certificate to CA",
|
||||||
|
caId: req.params.caId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:caId/issue-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue certificate from CA",
|
||||||
|
params: z.object({
|
||||||
|
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId)
|
||||||
|
}),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||||
|
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||||
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||||
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const { ttl, notAfter } = data;
|
||||||
|
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Either ttl or notAfter must be present, but not both",
|
||||||
|
path: ["ttl", "notAfter"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
|
||||||
|
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||||
|
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||||
|
await server.services.certificateAuthority.issueCertFromCa({
|
||||||
|
caId: req.params.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ISSUE_CERT,
|
||||||
|
metadata: {
|
||||||
|
caId: ca.id,
|
||||||
|
dn: ca.dn,
|
||||||
|
serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
privateKey,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CertificatesSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { CERTIFICATES } from "@app/lib/api-docs";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CrlReason } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get certificate",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: CertificatesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { cert, ca } = await server.services.certificate.getCert({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: cert
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:serialNumber/revoke",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Revoke",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string().trim(),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumberRes),
|
||||||
|
revokedAt: z.date().describe(CERTIFICATES.REVOKE.revokedAt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.REVOKE_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Successfully revoked certificate",
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
revokedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:serialNumber",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Delete certificate",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: CertificatesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { deletedCert, ca } = await server.services.certificate.deleteCert({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: deletedCert.id,
|
||||||
|
cn: deletedCert.commonName,
|
||||||
|
serialNumber: deletedCert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: deletedCert
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:serialNumber/certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Get certificate body of certificate",
|
||||||
|
params: z.object({
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||||
|
certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||||
|
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificate, certificateChain, serialNumber, cert, ca } = await server.services.certificate.getCertBody({
|
||||||
|
serialNumber: req.params.serialNumber,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_CERT,
|
||||||
|
metadata: {
|
||||||
|
certId: cert.id,
|
||||||
|
cn: cert.commonName,
|
||||||
|
serialNumber: cert.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { registerAdminRouter } from "./admin-router";
|
import { registerAdminRouter } from "./admin-router";
|
||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
|
import { registerCaRouter } from "./certificate-authority-router";
|
||||||
|
import { registerCertRouter } from "./certificate-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||||
@@ -17,7 +19,6 @@ import { registerProjectEnvRouter } from "./project-env-router";
|
|||||||
import { registerProjectKeyRouter } from "./project-key-router";
|
import { registerProjectKeyRouter } from "./project-key-router";
|
||||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerRateLimitRouter } from "./rate-limit-router";
|
|
||||||
import { registerSecretFolderRouter } from "./secret-folder-router";
|
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||||
import { registerSecretImportRouter } from "./secret-import-router";
|
import { registerSecretImportRouter } from "./secret-import-router";
|
||||||
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||||
@@ -44,7 +45,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||||
await server.register(registerRateLimitRouter, { prefix: "/rate-limit" });
|
|
||||||
await server.register(registerUserRouter, { prefix: "/user" });
|
await server.register(registerUserRouter, { prefix: "/user" });
|
||||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||||
@@ -63,6 +63,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (pkiRouter) => {
|
||||||
|
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||||
|
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||||
|
},
|
||||||
|
{ prefix: "/pki" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
||||||
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||||
|
|||||||
@@ -309,4 +309,32 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
return { membership };
|
return { membership };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:workspaceId/leave",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
membership: ProjectMembershipsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const membership = await server.services.projectMembership.leaveProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { membership };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSharingSchema } from "@app/db/schemas";
|
import { SecretSharingSchema } from "@app/db/schemas";
|
||||||
import { publicEndpointLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import {
|
||||||
|
publicEndpointLimit,
|
||||||
|
publicSecretShareCreationLimit,
|
||||||
|
readLimit,
|
||||||
|
writeLimit
|
||||||
|
} from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/",
|
url: "/public",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
@@ -82,9 +87,42 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z
|
expiresAt: z.string(),
|
||||||
.string()
|
expiresAfterViews: z.number()
|
||||||
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||||
|
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt: new Date(expiresAt),
|
||||||
|
expiresAfterViews
|
||||||
|
});
|
||||||
|
return { id: sharedSecret.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: publicSecretShareCreationLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
encryptedValue: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number()
|
expiresAfterViews: z.number()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspaceTags = await server.services.secretTag.getProjectTags({
|
const workspaceTags = await server.services.secretTag.getProjectTags({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -36,6 +36,67 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/tags/:tagId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId),
|
||||||
|
tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaceTag: SecretTagsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaceTag = await server.services.secretTag.getTagById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.tagId
|
||||||
|
});
|
||||||
|
return { workspaceTag };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/tags/slug/:tagSlug",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId),
|
||||||
|
tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaceTag: SecretTagsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaceTag = await server.services.secretTag.getTagBySlug({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
slug: req.params.tagSlug,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
return { workspaceTag };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:projectId/tags",
|
url: "/:projectId/tags",
|
||||||
@@ -57,7 +118,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspaceTag = await server.services.secretTag.createTag({
|
const workspaceTag = await server.services.secretTag.createTag({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -71,6 +132,42 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:projectId/tags/:tagId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId),
|
||||||
|
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
|
||||||
|
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
|
||||||
|
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaceTag: SecretTagsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaceTag = await server.services.secretTag.updateTag({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
id: req.params.tagId
|
||||||
|
});
|
||||||
|
return { workspaceTag };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:projectId/tags/:tagId",
|
url: "/:projectId/tags/:tagId",
|
||||||
@@ -88,7 +185,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const workspaceTag = await server.services.secretTag.deleteTag({
|
const workspaceTag = await server.services.secretTag.deleteTag({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -307,4 +308,80 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:slug/cas",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: slugSchema.describe("The slug of the project to list CAs.")
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
cas: z.array(CertificateAuthoritiesSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const cas = await server.services.project.listProjectCas({
|
||||||
|
filter: {
|
||||||
|
slug: req.params.slug,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
type: ProjectFilterType.SLUG
|
||||||
|
},
|
||||||
|
status: req.query.status,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type
|
||||||
|
});
|
||||||
|
return { cas };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:slug/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: slugSchema.describe("The slug of the project to list certificates.")
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).max(100).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(25)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificates: z.array(CertificatesSchema),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { certificates, totalCount } = await server.services.project.listProjectCertificates({
|
||||||
|
filter: {
|
||||||
|
slug: req.params.slug,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
type: ProjectFilterType.SLUG
|
||||||
|
},
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
return { certificates, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
SecretType,
|
SecretType,
|
||||||
ServiceTokenScopes
|
ServiceTokenScopes
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
@@ -259,18 +259,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
event: PostHogEventTypes.SecretPulled,
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
distinctId: getTelemetryDistinctId(req),
|
event: PostHogEventTypes.SecretPulled,
|
||||||
properties: {
|
distinctId: getTelemetryDistinctId(req),
|
||||||
numberOfSecrets: secrets.length,
|
properties: {
|
||||||
workspaceId,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
workspaceId,
|
||||||
secretPath: req.query.secretPath,
|
environment,
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
secretPath: req.query.secretPath,
|
||||||
...req.auditLogInfo
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
}
|
...req.auditLogInfo
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -306,7 +308,16 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
secret: secretRawSchema
|
secret: secretRawSchema.extend({
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
color: true
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -358,18 +369,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
event: PostHogEventTypes.SecretPulled,
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
distinctId: getTelemetryDistinctId(req),
|
event: PostHogEventTypes.SecretPulled,
|
||||||
properties: {
|
distinctId: getTelemetryDistinctId(req),
|
||||||
numberOfSecrets: 1,
|
properties: {
|
||||||
workspaceId: secret.workspace,
|
numberOfSecrets: 1,
|
||||||
environment,
|
workspaceId: secret.workspace,
|
||||||
secretPath: req.query.secretPath,
|
environment,
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
secretPath: req.query.secretPath,
|
||||||
...req.auditLogInfo
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
}
|
...req.auditLogInfo
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return { secret };
|
return { secret };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -404,6 +417,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||||
.describe(RAW_SECRETS.CREATE.secretValue),
|
.describe(RAW_SECRETS.CREATE.secretValue),
|
||||||
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
|
||||||
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
|
||||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type)
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type)
|
||||||
}),
|
}),
|
||||||
@@ -427,7 +441,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: req.body.type,
|
type: req.body.type,
|
||||||
secretValue: req.body.secretValue,
|
secretValue: req.body.secretValue,
|
||||||
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||||
secretComment: req.body.secretComment
|
secretComment: req.body.secretComment,
|
||||||
|
tagIds: req.body.tagIds
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -492,7 +507,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(RAW_SECRETS.UPDATE.secretPath),
|
.describe(RAW_SECRETS.UPDATE.secretPath),
|
||||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type)
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
|
||||||
|
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -513,7 +529,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretName: req.params.secretName,
|
secretName: req.params.secretName,
|
||||||
type: req.body.type,
|
type: req.body.type,
|
||||||
secretValue: req.body.secretValue,
|
secretValue: req.body.secretValue,
|
||||||
skipMultilineEncoding: req.body.skipMultilineEncoding
|
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||||
|
tagIds: req.body.tagIds
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -710,24 +727,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Move to telemetry plugin
|
// TODO: Move to telemetry plugin
|
||||||
let shouldRecordK8Event = false;
|
// let shouldRecordK8Event = false;
|
||||||
if (req.headers["user-agent"] === "k8-operatoer") {
|
// if (req.headers["user-agent"] === "k8-operatoer") {
|
||||||
const randomNumber = Math.random();
|
// const randomNumber = Math.random();
|
||||||
if (randomNumber > 0.95) {
|
// if (randomNumber > 0.95) {
|
||||||
shouldRecordK8Event = true;
|
// shouldRecordK8Event = true;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const shouldCapture =
|
const shouldCapture =
|
||||||
req.query.workspaceId !== "650e71fbae3e6c8572f436d4" &&
|
req.query.workspaceId !== "650e71fbae3e6c8572f436d4" && req.headers["user-agent"] !== "k8-operator";
|
||||||
(req.headers["user-agent"] !== "k8-operator" || shouldRecordK8Event);
|
|
||||||
const approximateNumberTotalSecrets = secrets.length * 20;
|
|
||||||
if (shouldCapture) {
|
if (shouldCapture) {
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
event: PostHogEventTypes.SecretPulled,
|
event: PostHogEventTypes.SecretPulled,
|
||||||
distinctId: getTelemetryDistinctId(req),
|
distinctId: getTelemetryDistinctId(req),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: shouldRecordK8Event ? approximateNumberTotalSecrets : secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
workspaceId: req.query.workspaceId,
|
workspaceId: req.query.workspaceId,
|
||||||
environment: req.query.environment,
|
environment: req.query.environment,
|
||||||
secretPath: req.query.secretPath,
|
secretPath: req.query.secretPath,
|
||||||
@@ -804,18 +819,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
event: PostHogEventTypes.SecretPulled,
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
distinctId: getTelemetryDistinctId(req),
|
event: PostHogEventTypes.SecretPulled,
|
||||||
properties: {
|
distinctId: getTelemetryDistinctId(req),
|
||||||
numberOfSecrets: 1,
|
properties: {
|
||||||
workspaceId: req.query.workspaceId,
|
numberOfSecrets: 1,
|
||||||
environment: req.query.environment,
|
workspaceId: req.query.workspaceId,
|
||||||
secretPath: req.query.secretPath,
|
environment: req.query.environment,
|
||||||
channel: getUserAgentType(req.headers["user-agent"]),
|
secretPath: req.query.secretPath,
|
||||||
...req.auditLogInfo
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
}
|
...req.auditLogInfo
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return { secret };
|
return { secret };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthorityCertDALFactory = ReturnType<typeof certificateAuthorityCertDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityCertDALFactory = (db: TDbClient) => {
|
||||||
|
const caCertOrm = ormify(db, TableName.CertificateAuthorityCert);
|
||||||
|
return caCertOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
||||||
|
const caOrm = ormify(db, TableName.CertificateAuthority);
|
||||||
|
|
||||||
|
// note: not used
|
||||||
|
const buildCertificateChain = async (caId: string) => {
|
||||||
|
try {
|
||||||
|
const result: {
|
||||||
|
caId: string;
|
||||||
|
parentCaId?: string;
|
||||||
|
encryptedCertificate: Buffer;
|
||||||
|
}[] = await db
|
||||||
|
.withRecursive("cte", (cte) => {
|
||||||
|
void cte
|
||||||
|
.select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate")
|
||||||
|
.from({ ca: TableName.CertificateAuthority })
|
||||||
|
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
|
||||||
|
.where("ca.id", caId)
|
||||||
|
.unionAll((builder) => {
|
||||||
|
void builder
|
||||||
|
.select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate")
|
||||||
|
.from({ ca: TableName.CertificateAuthority })
|
||||||
|
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
|
||||||
|
.innerJoin("cte", "cte.parentCaId", "ca.id");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.select("*")
|
||||||
|
.from("cte");
|
||||||
|
|
||||||
|
// Extract certificates and reverse the order to have the root CA at the end
|
||||||
|
const certChain: Buffer[] = result.map((row) => row.encryptedCertificate);
|
||||||
|
return certChain;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "BuildCertificateChain" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...caOrm,
|
||||||
|
buildCertificateChain
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||||
|
import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types";
|
||||||
|
|
||||||
|
export const createDistinguishedName = (parts: TDNParts) => {
|
||||||
|
const dnParts = [];
|
||||||
|
if (parts.country) dnParts.push(`C=${parts.country}`);
|
||||||
|
if (parts.organization) dnParts.push(`O=${parts.organization}`);
|
||||||
|
if (parts.ou) dnParts.push(`OU=${parts.ou}`);
|
||||||
|
if (parts.province) dnParts.push(`ST=${parts.province}`);
|
||||||
|
if (parts.commonName) dnParts.push(`CN=${parts.commonName}`);
|
||||||
|
if (parts.locality) dnParts.push(`L=${parts.locality}`);
|
||||||
|
return dnParts.join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
||||||
|
switch (keyAlgorithm) {
|
||||||
|
case CertKeyAlgorithm.RSA_4096:
|
||||||
|
return {
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
modulusLength: 4096
|
||||||
|
};
|
||||||
|
case CertKeyAlgorithm.ECDSA_P256:
|
||||||
|
return {
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-256",
|
||||||
|
hash: "SHA-256"
|
||||||
|
};
|
||||||
|
case CertKeyAlgorithm.ECDSA_P384:
|
||||||
|
return {
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-384",
|
||||||
|
hash: "SHA-384"
|
||||||
|
};
|
||||||
|
default: {
|
||||||
|
// RSA_2048
|
||||||
|
return {
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
modulusLength: 2048
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the public and private key of CA with id [caId]
|
||||||
|
* Note: credentials are returned as crypto.webcrypto.CryptoKey
|
||||||
|
* suitable for use with @peculiar/x509 module
|
||||||
|
*/
|
||||||
|
export const getCaCredentials = async ({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: TGetCaCredentialsDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
|
||||||
|
if (!caSecret) throw new BadRequestError({ message: "CA secret not found" });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPrivateKey = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
|
||||||
|
const caPrivateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
skObj.export({ format: "der", type: "pkcs8" }),
|
||||||
|
alg,
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const pkObj = crypto.createPublicKey(skObj);
|
||||||
|
const caPublicKey = await crypto.subtle.importKey("spki", pkObj.export({ format: "der", type: "spki" }), alg, true, [
|
||||||
|
"verify"
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
caPrivateKey,
|
||||||
|
caPublicKey
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the decrypted pem-encoded certificate and certificate chain
|
||||||
|
* for CA with id [caId].
|
||||||
|
*/
|
||||||
|
export const getCaCertChain = async ({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: TGetCaCertChainDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
|
||||||
|
const decryptedChain = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificateChain
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
caCert: caCertObj.toString("pem"),
|
||||||
|
caCertChain: decryptedChain.toString("utf-8"),
|
||||||
|
serialNumber: caCertObj.serialNumber
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the certificate revocation list (CRL)
|
||||||
|
* for CA with id [caId]
|
||||||
|
*/
|
||||||
|
export const rebuildCaCrl = async ({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
certificateDAL,
|
||||||
|
kmsService
|
||||||
|
}: TRebuildCaCrlDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const privateKey = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
||||||
|
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||||
|
"sign"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const revokedCerts = await certificateDAL.find({
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.REVOKED
|
||||||
|
});
|
||||||
|
|
||||||
|
const crl = await x509.X509CrlGenerator.create({
|
||||||
|
issuer: ca.dn,
|
||||||
|
thisUpdate: new Date(),
|
||||||
|
nextUpdate: new Date("2025/12/12"),
|
||||||
|
entries: revokedCerts.map((revokedCert) => {
|
||||||
|
return {
|
||||||
|
serialNumber: revokedCert.serialNumber,
|
||||||
|
revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||||
|
reason: revokedCert.revocationReason as number,
|
||||||
|
invalidity: new Date("2022/01/01"),
|
||||||
|
issuer: ca.dn
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
signingKey: sk
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCrlDAL.update(
|
||||||
|
{
|
||||||
|
caId: ca.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encryptedCrl
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { CertKeyAlgorithm, CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
|
import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types";
|
||||||
|
|
||||||
|
type TCertificateAuthorityQueueFactoryDep = {
|
||||||
|
// TODO: Pick
|
||||||
|
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||||
|
certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;
|
||||||
|
certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
};
|
||||||
|
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityQueueFactory = ({
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
queueService
|
||||||
|
}: TCertificateAuthorityQueueFactoryDep) => {
|
||||||
|
// TODO 1: auto-periodic rotation
|
||||||
|
// TODO 2: manual rotation
|
||||||
|
|
||||||
|
const setCaCrlRotationInterval = async ({ caId, rotationIntervalDays }: TRotateCaCrlTriggerDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// query for config
|
||||||
|
// const caCrl = await certificateAuthorityCrlDAL.findOne({
|
||||||
|
// caId
|
||||||
|
// });
|
||||||
|
|
||||||
|
await queueService.queue(
|
||||||
|
// TODO: clarify queue + job naming
|
||||||
|
QueueName.CaCrlRotation,
|
||||||
|
QueueJobs.CaCrlRotation,
|
||||||
|
{
|
||||||
|
caId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jobId: `ca-crl-rotation-${caId}`,
|
||||||
|
repeat: {
|
||||||
|
// on prod it this will be in days, in development this will be second
|
||||||
|
every:
|
||||||
|
appCfg.NODE_ENV === "development"
|
||||||
|
? secondsToMillis(rotationIntervalDays)
|
||||||
|
: daysToMillisecond(rotationIntervalDays),
|
||||||
|
immediately: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.CaCrlRotation, async (job) => {
|
||||||
|
const { caId } = job.data;
|
||||||
|
logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`);
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const privateKey = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
||||||
|
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
|
||||||
|
"sign"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const revokedCerts = await certificateDAL.find({
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.REVOKED
|
||||||
|
});
|
||||||
|
|
||||||
|
const crl = await x509.X509CrlGenerator.create({
|
||||||
|
issuer: ca.dn,
|
||||||
|
thisUpdate: new Date(),
|
||||||
|
nextUpdate: new Date("2025/12/12"), // TODO: depends on configured rebuild interval
|
||||||
|
entries: revokedCerts.map((revokedCert) => {
|
||||||
|
return {
|
||||||
|
serialNumber: revokedCert.serialNumber,
|
||||||
|
revocationDate: new Date(revokedCert.revokedAt as Date),
|
||||||
|
reason: revokedCert.revocationReason as number,
|
||||||
|
invalidity: new Date("2022/01/01"),
|
||||||
|
issuer: ca.dn
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
signingKey: sk
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCrlDAL.update(
|
||||||
|
{
|
||||||
|
caId: ca.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encryptedCrl
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
queueService.listen(QueueName.CaCrlRotation, "failed", (job, err) => {
|
||||||
|
logger.error(err, "Failed to rotate CA CRL %s", job?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
setCaCrlRotationInterval
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateAuthoritySecretDALFactory = ReturnType<typeof certificateAuthoritySecretDALFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthoritySecretDALFactory = (db: TDbClient) => {
|
||||||
|
const caSecretOrm = ormify(db, TableName.CertificateAuthoritySecret);
|
||||||
|
return caSecretOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,821 @@
|
|||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import crypto, { KeyObject } from "crypto";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||||
|
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import {
|
||||||
|
createDistinguishedName,
|
||||||
|
getCaCertChain,
|
||||||
|
getCaCredentials,
|
||||||
|
keyAlgorithmToAlgCfg
|
||||||
|
} from "./certificate-authority-fns";
|
||||||
|
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
|
import {
|
||||||
|
CaStatus,
|
||||||
|
CaType,
|
||||||
|
TCreateCaDTO,
|
||||||
|
TDeleteCaDTO,
|
||||||
|
TGetCaCertDTO,
|
||||||
|
TGetCaCsrDTO,
|
||||||
|
TGetCaDTO,
|
||||||
|
TImportCertToCaDTO,
|
||||||
|
TIssueCertFromCaDTO,
|
||||||
|
TSignIntermediateDTO,
|
||||||
|
TUpdateCaDTO
|
||||||
|
} from "./certificate-authority-types";
|
||||||
|
|
||||||
|
type TCertificateAuthorityServiceFactoryDep = {
|
||||||
|
certificateAuthorityDAL: Pick<
|
||||||
|
TCertificateAuthorityDALFactory,
|
||||||
|
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
|
||||||
|
>;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||||
|
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
||||||
|
|
||||||
|
export const certificateAuthorityServiceFactory = ({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
}: TCertificateAuthorityServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Generates new root or intermediate CA
|
||||||
|
*/
|
||||||
|
const createCa = async ({
|
||||||
|
projectSlug,
|
||||||
|
type,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
organization,
|
||||||
|
ou,
|
||||||
|
country,
|
||||||
|
province,
|
||||||
|
locality,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
maxPathLength,
|
||||||
|
keyAlgorithm,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateCaDTO) => {
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const dn = createDistinguishedName({
|
||||||
|
commonName,
|
||||||
|
organization,
|
||||||
|
ou,
|
||||||
|
country,
|
||||||
|
province,
|
||||||
|
locality
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||||
|
const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const newCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
|
||||||
|
// if undefined, set [notAfterDate] to 10 years from now
|
||||||
|
const notAfterDate = notAfter
|
||||||
|
? new Date(notAfter)
|
||||||
|
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
type,
|
||||||
|
organization,
|
||||||
|
ou,
|
||||||
|
country,
|
||||||
|
province,
|
||||||
|
locality,
|
||||||
|
friendlyName: friendlyName || dn,
|
||||||
|
commonName,
|
||||||
|
status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE,
|
||||||
|
dn,
|
||||||
|
keyAlgorithm,
|
||||||
|
...(type === CaType.ROOT && {
|
||||||
|
maxPathLength,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
serialNumber
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: project.id,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type === CaType.ROOT) {
|
||||||
|
// note: create self-signed cert only applicable for root CA
|
||||||
|
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
|
name: dn,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
keys,
|
||||||
|
extensions: [
|
||||||
|
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||||
|
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(cert.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.alloc(0)
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCertDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create empty CRL
|
||||||
|
const crl = await x509.X509CrlGenerator.create({
|
||||||
|
issuer: ca.dn,
|
||||||
|
thisUpdate: new Date(),
|
||||||
|
nextUpdate: new Date("2025/12/12"), // TODO: change
|
||||||
|
entries: [],
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
signingKey: keys.privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCrlDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedCrl
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
|
||||||
|
const skObj = KeyObject.from(keys.privateKey);
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: skObj.export({
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "der"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthoritySecretDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedPrivateKey
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update CA with id [caId].
|
||||||
|
* Note: Used to enable/disable CA
|
||||||
|
*/
|
||||||
|
const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status });
|
||||||
|
|
||||||
|
return updatedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete CA with id [caId]
|
||||||
|
*/
|
||||||
|
const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCa = await certificateAuthorityDAL.deleteById(caId);
|
||||||
|
|
||||||
|
return deletedCa;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return certificate signing request (CSR) made with CA with id [caId]
|
||||||
|
*/
|
||||||
|
const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
|
||||||
|
|
||||||
|
const { caPrivateKey, caPublicKey } = await getCaCredentials({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||||
|
name: ca.dn,
|
||||||
|
keys: {
|
||||||
|
privateKey: caPrivateKey,
|
||||||
|
publicKey: caPublicKey
|
||||||
|
},
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment
|
||||||
|
)
|
||||||
|
],
|
||||||
|
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
csr: csrObj.toString("pem"),
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return certificate and certificate chain for CA
|
||||||
|
*/
|
||||||
|
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const { caCert, caCertChain, serialNumber } = await getCaCertChain({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: caCert,
|
||||||
|
certificateChain: caCertChain,
|
||||||
|
serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue certificate to be imported back in for intermediate CA
|
||||||
|
*/
|
||||||
|
const signIntermediate = async ({
|
||||||
|
caId,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
csr,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
maxPathLength
|
||||||
|
}: TSignIntermediateDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
const decryptedCaCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
|
||||||
|
// check path length constraint
|
||||||
|
const caPathLength = caCertObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||||
|
if (caPathLength !== undefined) {
|
||||||
|
if (caPathLength === 0)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to issue intermediate certificate due to CA path length constraint"
|
||||||
|
});
|
||||||
|
if (maxPathLength >= caPathLength || (maxPathLength === -1 && caPathLength !== -1))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The requested path length constraint exceeds the CA's allowed path length"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
const notAfterDate = new Date(notAfter);
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { caPrivateKey } = await getCaCredentials({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: csrObj.subject,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: csrObj.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
x509.KeyUsageFlags.keyCertSign |
|
||||||
|
x509.KeyUsageFlags.cRLSign |
|
||||||
|
x509.KeyUsageFlags.digitalSignature |
|
||||||
|
x509.KeyUsageFlags.keyEncipherment,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caId,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: intermediateCert.toString("pem"),
|
||||||
|
issuingCaCertificate,
|
||||||
|
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||||
|
serialNumber: intermediateCert.serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import certificate for (un-installed) CA with id [caId].
|
||||||
|
* Note: Can be used to import an external certificate and certificate chain
|
||||||
|
* to be installed into the CA.
|
||||||
|
*/
|
||||||
|
const importCertToCa = async ({
|
||||||
|
caId,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
certificate,
|
||||||
|
certificateChain
|
||||||
|
}: TImportCertToCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
|
||||||
|
|
||||||
|
const certObj = new x509.X509Certificate(certificate);
|
||||||
|
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||||
|
|
||||||
|
// validate imported certificate and certificate chain
|
||||||
|
const certificates = certificateChain
|
||||||
|
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||||
|
?.map((cert) => new x509.X509Certificate(cert));
|
||||||
|
|
||||||
|
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
|
|
||||||
|
const chain = new x509.X509ChainBuilder({
|
||||||
|
certificates
|
||||||
|
});
|
||||||
|
|
||||||
|
const chainItems = await chain.build(certObj);
|
||||||
|
|
||||||
|
// chain.build() implicitly verifies the chain
|
||||||
|
if (chainItems.length !== certificates.length + 1)
|
||||||
|
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||||
|
|
||||||
|
const parentCertObj = chainItems[1];
|
||||||
|
const parentCertSubject = parentCertObj.subject;
|
||||||
|
|
||||||
|
const parentCa = await certificateAuthorityDAL.findOne({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
dn: parentCertSubject
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(certificateChain)
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateAuthorityCertDAL.transaction(async (tx) => {
|
||||||
|
await certificateAuthorityCertDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateAuthorityDAL.updateById(
|
||||||
|
ca.id,
|
||||||
|
{
|
||||||
|
status: CaStatus.ACTIVE,
|
||||||
|
maxPathLength: maxPathLength === undefined ? -1 : maxPathLength,
|
||||||
|
notBefore: new Date(certObj.notBefore),
|
||||||
|
notAfter: new Date(certObj.notAfter),
|
||||||
|
serialNumber: certObj.serialNumber,
|
||||||
|
parentCaId: parentCa?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ca };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new leaf certificate issued by CA with id [caId]
|
||||||
|
*/
|
||||||
|
const issueCertFromCa = async ({
|
||||||
|
caId,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
ttl,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueCertFromCaDTO) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
|
||||||
|
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
|
||||||
|
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
|
||||||
|
|
||||||
|
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
|
||||||
|
if (notAfter) {
|
||||||
|
notAfterDate = new Date(notAfter);
|
||||||
|
} else if (ttl) {
|
||||||
|
notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||||
|
}
|
||||||
|
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||||
|
name: `CN=${commonName}`,
|
||||||
|
keys: leafKeys,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||||
|
],
|
||||||
|
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caPrivateKey } = await getCaCredentials({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialNumber = crypto.randomBytes(32).toString("hex");
|
||||||
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: csrObj.subject,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: csrObj.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||||
|
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await certificateDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
friendlyName: friendlyName || commonName,
|
||||||
|
commonName,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedCertificate
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: leafCert.toString("pem"),
|
||||||
|
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||||
|
issuingCaCertificate,
|
||||||
|
privateKey: skLeaf,
|
||||||
|
serialNumber,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createCa,
|
||||||
|
getCaById,
|
||||||
|
updateCaById,
|
||||||
|
deleteCaById,
|
||||||
|
getCaCsr,
|
||||||
|
getCaCert,
|
||||||
|
signIntermediate,
|
||||||
|
importCertToCa,
|
||||||
|
issueCertFromCa
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { CertKeyAlgorithm } from "../certificate/certificate-types";
|
||||||
|
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
|
|
||||||
|
export enum CaType {
|
||||||
|
ROOT = "root",
|
||||||
|
INTERMEDIATE = "intermediate"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CaStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
DISABLED = "disabled",
|
||||||
|
PENDING_CERTIFICATE = "pending-certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateCaDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
type: CaType;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName: string;
|
||||||
|
organization: string;
|
||||||
|
ou: string;
|
||||||
|
country: string;
|
||||||
|
province: string;
|
||||||
|
locality: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
maxPathLength: number;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
status?: CaStatus;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCaCsrDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCaCertDTO = {
|
||||||
|
caId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TSignIntermediateDTO = {
|
||||||
|
caId: string;
|
||||||
|
csr: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter: string;
|
||||||
|
maxPathLength: number;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TImportCertToCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueCertFromCaDTO = {
|
||||||
|
caId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName: string;
|
||||||
|
ttl: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDNParts = {
|
||||||
|
commonName?: string;
|
||||||
|
organization?: string;
|
||||||
|
ou?: string;
|
||||||
|
country?: string;
|
||||||
|
province?: string;
|
||||||
|
locality?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCaCredentialsDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCaCertChainDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRebuildCaCrlDTO = {
|
||||||
|
caId: string;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decrypt" | "encrypt">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRotateCaCrlTriggerDTO = {
|
||||||
|
caId: string;
|
||||||
|
rotationIntervalDays: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const isValidDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return !Number.isNaN(date.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateBodyDALFactory = ReturnType<typeof certificateBodyDALFactory>;
|
||||||
|
|
||||||
|
export const certificateBodyDALFactory = (db: TDbClient) => {
|
||||||
|
const certificateBodyOrm = ormify(db, TableName.CertificateBody);
|
||||||
|
return certificateBodyOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
|
||||||
|
|
||||||
|
export const certificateDALFactory = (db: TDbClient) => {
|
||||||
|
const certificateOrm = ormify(db, TableName.Certificate);
|
||||||
|
|
||||||
|
const countCertificatesInProject = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await db(TableName.Certificate)
|
||||||
|
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
|
||||||
|
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.where(`${TableName.Project}.id`, projectId)
|
||||||
|
.count("*")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Count all project certificates" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...certificateOrm,
|
||||||
|
countCertificatesInProject
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { CrlReason } from "./certificate-types";
|
||||||
|
|
||||||
|
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||||
|
switch (crlReason) {
|
||||||
|
case CrlReason.KEY_COMPROMISE:
|
||||||
|
return x509.X509CrlReason.keyCompromise;
|
||||||
|
case CrlReason.CA_COMPROMISE:
|
||||||
|
return x509.X509CrlReason.cACompromise;
|
||||||
|
case CrlReason.AFFILIATION_CHANGED:
|
||||||
|
return x509.X509CrlReason.affiliationChanged;
|
||||||
|
case CrlReason.SUPERSEDED:
|
||||||
|
return x509.X509CrlReason.superseded;
|
||||||
|
case CrlReason.CESSATION_OF_OPERATION:
|
||||||
|
return x509.X509CrlReason.cessationOfOperation;
|
||||||
|
case CrlReason.CERTIFICATE_HOLD:
|
||||||
|
return x509.X509CrlReason.certificateHold;
|
||||||
|
case CrlReason.PRIVILEGE_WITHDRAWN:
|
||||||
|
return x509.X509CrlReason.privilegeWithdrawn;
|
||||||
|
case CrlReason.A_A_COMPROMISE:
|
||||||
|
return x509.X509CrlReason.aACompromise;
|
||||||
|
default:
|
||||||
|
return x509.X509CrlReason.unspecified;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
|
||||||
|
import { revocationReasonToCrlCode } from "./certificate-fns";
|
||||||
|
import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types";
|
||||||
|
|
||||||
|
type TCertificateServiceFactoryDep = {
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encrypt" | "decrypt">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFactory>;
|
||||||
|
|
||||||
|
export const certificateServiceFactory = ({
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
}: TCertificateServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Return details for certificate with serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete certificate with serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedCert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke certificate with serial number [serialNumber].
|
||||||
|
* Note: Revoking a certificate adds it to the certificate revocation list (CRL)
|
||||||
|
* of its issuing CA
|
||||||
|
*/
|
||||||
|
const revokeCert = async ({
|
||||||
|
serialNumber,
|
||||||
|
revocationReason,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TRevokeCertDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked");
|
||||||
|
|
||||||
|
const revokedAt = new Date();
|
||||||
|
await certificateDAL.update(
|
||||||
|
{
|
||||||
|
id: cert.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: CertStatus.REVOKED,
|
||||||
|
revokedAt,
|
||||||
|
revocationReason: revocationReasonToCrlCode(revocationReason)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// rebuild CRL (TODO: move to interval-based cron job)
|
||||||
|
await rebuildCaCrl({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
certificateDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return { revokedAt, cert, ca };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return certificate body and certificate chain for certificate with
|
||||||
|
* serial number [serialNumber]
|
||||||
|
*/
|
||||||
|
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
|
||||||
|
const cert = await certificateDAL.findOne({ serialNumber });
|
||||||
|
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
const keyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCert = await kmsService.decrypt({
|
||||||
|
kmsId: keyId,
|
||||||
|
cipherTextBlob: certBody.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const certObj = new x509.X509Certificate(decryptedCert);
|
||||||
|
|
||||||
|
const { caCert, caCertChain } = await getCaCertChain({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: certObj.toString("pem"),
|
||||||
|
certificateChain: `${caCert}\n${caCertChain}`.trim(),
|
||||||
|
serialNumber: certObj.serialNumber,
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCert,
|
||||||
|
deleteCert,
|
||||||
|
revokeCert,
|
||||||
|
getCertBody
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum CertStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
REVOKED = "revoked"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertKeyAlgorithm {
|
||||||
|
RSA_2048 = "RSA_2048",
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECDSA_P256 = "EC_prime256v1",
|
||||||
|
ECDSA_P384 = "EC_secp384r1"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CrlReason {
|
||||||
|
UNSPECIFIED = "UNSPECIFIED",
|
||||||
|
KEY_COMPROMISE = "KEY_COMPROMISE",
|
||||||
|
CA_COMPROMISE = "CA_COMPROMISE",
|
||||||
|
AFFILIATION_CHANGED = "AFFILIATION_CHANGED",
|
||||||
|
SUPERSEDED = "SUPERSEDED",
|
||||||
|
CESSATION_OF_OPERATION = "CESSATION_OF_OPERATION",
|
||||||
|
CERTIFICATE_HOLD = "CERTIFICATE_HOLD",
|
||||||
|
// REMOVE_FROM_CRL = "REMOVE_FROM_CRL",
|
||||||
|
PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
|
||||||
|
A_A_COMPROMISE = "A_A_COMPROMISE"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TGetCertDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteCertDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TRevokeCertDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
revocationReason: CrlReason;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetCertBodyDTO = {
|
||||||
|
serialNumber: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
UpdateSecretCommand
|
UpdateSecretCommand
|
||||||
} from "@aws-sdk/client-secrets-manager";
|
} from "@aws-sdk/client-secrets-manager";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import AWS from "aws-sdk";
|
import AWS, { AWSError } from "aws-sdk";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import sodium from "libsodium-wrappers";
|
import sodium from "libsodium-wrappers";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
@@ -452,7 +452,11 @@ const syncSecretsAWSParameterStore = async ({
|
|||||||
accessId: string | null;
|
accessId: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!accessId) return;
|
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
||||||
|
|
||||||
|
if (!accessId) {
|
||||||
|
throw new Error("AWS access ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
const config = new AWS.Config({
|
const config = new AWS.Config({
|
||||||
region: integration.region as string,
|
region: integration.region as string,
|
||||||
@@ -557,6 +561,11 @@ const syncSecretsAWSParameterStore = async ({
|
|||||||
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
|
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response = {
|
||||||
|
isSynced: false,
|
||||||
|
syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,6 +594,8 @@ const syncSecretsAWSParameterStore = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -603,7 +614,9 @@ const syncSecretsAWSSecretManager = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
||||||
|
|
||||||
if (!accessId) return;
|
if (!accessId) {
|
||||||
|
throw new Error("AWS access ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
const secretsManager = new SecretsManagerClient({
|
const secretsManager = new SecretsManagerClient({
|
||||||
region: integration.region as string,
|
region: integration.region as string,
|
||||||
@@ -722,7 +735,7 @@ const syncSecretsAWSSecretManager = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// case when AWS manager can't find the specified secret
|
// case 1: when AWS manager can't find the specified secret
|
||||||
if (err instanceof ResourceNotFoundException && secretsManager) {
|
if (err instanceof ResourceNotFoundException && secretsManager) {
|
||||||
await secretsManager.send(
|
await secretsManager.send(
|
||||||
new CreateSecretCommand({
|
new CreateSecretCommand({
|
||||||
@@ -734,6 +747,9 @@ const syncSecretsAWSSecretManager = async ({
|
|||||||
: []
|
: []
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
// case 2: something unexpected went wrong, so we'll throw the error to reflect the error in the integration sync status
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -753,14 +769,12 @@ const syncSecretsAWSSecretManager = async ({
|
|||||||
const syncSecretsHeroku = async ({
|
const syncSecretsHeroku = async ({
|
||||||
createManySecretsRawFn,
|
createManySecretsRawFn,
|
||||||
updateManySecretsRawFn,
|
updateManySecretsRawFn,
|
||||||
integrationDAL,
|
|
||||||
integration,
|
integration,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken
|
||||||
}: {
|
}: {
|
||||||
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||||
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
|
||||||
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
|
|
||||||
integration: TIntegrations & {
|
integration: TIntegrations & {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
environment: {
|
environment: {
|
||||||
@@ -862,10 +876,6 @@ const syncSecretsHeroku = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await integrationDAL.updateById(integration.id, {
|
|
||||||
lastUsed: new Date()
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2656,7 +2666,9 @@ const syncSecretsHashiCorpVault = async ({
|
|||||||
accessId: string | null;
|
accessId: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!accessId) return;
|
if (!accessId) {
|
||||||
|
throw new Error("Access ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
interface LoginAppRoleRes {
|
interface LoginAppRoleRes {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -3486,6 +3498,8 @@ export const syncIntegrationSecrets = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
appendices?: { prefix: string; suffix: string };
|
appendices?: { prefix: string; suffix: string };
|
||||||
}) => {
|
}) => {
|
||||||
|
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
||||||
|
|
||||||
switch (integration.integration) {
|
switch (integration.integration) {
|
||||||
case Integrations.GCP_SECRET_MANAGER:
|
case Integrations.GCP_SECRET_MANAGER:
|
||||||
await syncSecretsGCPSecretManager({
|
await syncSecretsGCPSecretManager({
|
||||||
@@ -3502,7 +3516,7 @@ export const syncIntegrationSecrets = async ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case Integrations.AWS_PARAMETER_STORE:
|
case Integrations.AWS_PARAMETER_STORE:
|
||||||
await syncSecretsAWSParameterStore({
|
response = await syncSecretsAWSParameterStore({
|
||||||
integration,
|
integration,
|
||||||
secrets,
|
secrets,
|
||||||
accessId,
|
accessId,
|
||||||
@@ -3521,7 +3535,6 @@ export const syncIntegrationSecrets = async ({
|
|||||||
await syncSecretsHeroku({
|
await syncSecretsHeroku({
|
||||||
createManySecretsRawFn,
|
createManySecretsRawFn,
|
||||||
updateManySecretsRawFn,
|
updateManySecretsRawFn,
|
||||||
integrationDAL,
|
|
||||||
integration,
|
integration,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken
|
||||||
@@ -3727,4 +3740,6 @@ export const syncIntegrationSecrets = async ({
|
|||||||
default:
|
default:
|
||||||
throw new BadRequestError({ message: "Invalid integration" });
|
throw new BadRequestError({ message: "Invalid integration" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,19 +29,22 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe
|
|||||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||||
|
|
||||||
// this is used symmetric encryption
|
// this is used symmetric encryption
|
||||||
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true }: TGenerateKMSDTO) => {
|
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true, tx }: TGenerateKMSDTO) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
const kmsKeyMaterial = randomSecureBytes(32);
|
const kmsKeyMaterial = randomSecureBytes(32);
|
||||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
const { encryptedKey, ...doc } = await kmsDAL.create({
|
const { encryptedKey, ...doc } = await kmsDAL.create(
|
||||||
version: 1,
|
{
|
||||||
encryptedKey: encryptedKeyMaterial,
|
version: 1,
|
||||||
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
encryptedKey: encryptedKeyMaterial,
|
||||||
isReserved,
|
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
||||||
orgId: scopeType === "org" ? scopeId : undefined,
|
isReserved,
|
||||||
projectId: scopeType === "project" ? scopeId : undefined
|
orgId: scopeType === "org" ? scopeId : undefined,
|
||||||
});
|
projectId: scopeType === "project" ? scopeId : undefined
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
return doc;
|
return doc;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
export type TGenerateKMSDTO = {
|
export type TGenerateKMSDTO = {
|
||||||
scopeType: "project" | "org";
|
scopeType: "project" | "org";
|
||||||
scopeId: string;
|
scopeId: string;
|
||||||
isReserved?: boolean;
|
isReserved?: boolean;
|
||||||
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TEncryptWithKmsDTO = {
|
export type TEncryptWithKmsDTO = {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
TDeleteProjectMembershipsDTO,
|
TDeleteProjectMembershipsDTO,
|
||||||
TGetProjectMembershipByUsernameDTO,
|
TGetProjectMembershipByUsernameDTO,
|
||||||
TGetProjectMembershipDTO,
|
TGetProjectMembershipDTO,
|
||||||
|
TLeaveProjectDTO,
|
||||||
TUpdateProjectMembershipDTO
|
TUpdateProjectMembershipDTO
|
||||||
} from "./project-membership-types";
|
} from "./project-membership-types";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
||||||
@@ -531,6 +532,53 @@ export const projectMembershipServiceFactory = ({
|
|||||||
return memberships;
|
return memberships;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const leaveProject = async ({ projectId, actorId, actor }: TLeaveProjectDTO) => {
|
||||||
|
if (actor !== ActorType.USER) {
|
||||||
|
throw new BadRequestError({ message: "Only users can leave projects" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
|
if (project.version !== ProjectVersion.V2) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Please ask your project administrator to upgrade the project before leaving."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
|
|
||||||
|
if (!projectMembers?.length) {
|
||||||
|
throw new BadRequestError({ message: "Failed to find project members" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectMembers.length < 2) {
|
||||||
|
throw new BadRequestError({ message: "You cannot leave the project as you are the only member" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminMembers = projectMembers.filter(
|
||||||
|
(member) => member.roles.map((r) => r.role).includes("admin") && member.userId !== actorId
|
||||||
|
);
|
||||||
|
if (!adminMembers.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "You cannot leave the project as you are the only admin. Promote another user to admin before leaving."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedMembership = (
|
||||||
|
await projectMembershipDAL.delete({
|
||||||
|
projectId: project.id,
|
||||||
|
userId: actorId
|
||||||
|
})
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (!deletedMembership) {
|
||||||
|
throw new BadRequestError({ message: "Failed to leave project" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedMembership;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getProjectMemberships,
|
getProjectMemberships,
|
||||||
getProjectMembershipByUsername,
|
getProjectMembershipByUsername,
|
||||||
@@ -538,6 +586,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
addUsersToProjectNonE2EE,
|
addUsersToProjectNonE2EE,
|
||||||
deleteProjectMemberships,
|
deleteProjectMemberships,
|
||||||
deleteProjectMembership, // TODO: Remove this
|
deleteProjectMembership, // TODO: Remove this
|
||||||
addUsersToProject
|
addUsersToProject,
|
||||||
|
leaveProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||||
|
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
|
||||||
export enum ProjectUserMembershipTemporaryMode {
|
export enum ProjectUserMembershipTemporaryMode {
|
||||||
Relative = "relative"
|
Relative = "relative"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
import { AddUserToWsDTO } from "./project-types";
|
import { AddUserToWsDTO } from "./project-types";
|
||||||
|
|
||||||
@@ -49,3 +52,44 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
|
|||||||
|
|
||||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProjectKmsCertificateKeyId = async ({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey">;
|
||||||
|
}) => {
|
||||||
|
const keyId = await projectDAL.transaction(async (tx) => {
|
||||||
|
const project = await projectDAL.findOne({ id: projectId }, tx);
|
||||||
|
if (!project) {
|
||||||
|
throw new BadRequestError({ message: "Project not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!project.kmsCertificateKeyId) {
|
||||||
|
// create default kms key for certificate service
|
||||||
|
const key = await kmsService.generateKmsKey({
|
||||||
|
scopeId: projectId,
|
||||||
|
scopeType: "project",
|
||||||
|
isReserved: true,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectDAL.updateById(
|
||||||
|
projectId,
|
||||||
|
{
|
||||||
|
kmsCertificateKeyId: key.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return key.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.kmsCertificateKeyId;
|
||||||
|
});
|
||||||
|
|
||||||
|
return keyId;
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
@@ -36,6 +38,8 @@ import {
|
|||||||
TCreateProjectDTO,
|
TCreateProjectDTO,
|
||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
|
TListProjectCasDTO,
|
||||||
|
TListProjectCertsDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
@@ -50,6 +54,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
|
// TODO: Pick
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
@@ -63,6 +68,8 @@ type TProjectServiceFactoryDep = {
|
|||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
@@ -90,6 +97,8 @@ export const projectServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
keyStore
|
keyStore
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
@@ -523,6 +532,83 @@ export const projectServiceFactory = ({
|
|||||||
return project.upgradeStatus || null;
|
return project.upgradeStatus || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of CAs for project
|
||||||
|
*/
|
||||||
|
const listProjectCas = async ({
|
||||||
|
status,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
filter,
|
||||||
|
actor
|
||||||
|
}: TListProjectCasDTO) => {
|
||||||
|
const project = await projectDAL.findProjectByFilter(filter);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const cas = await certificateAuthorityDAL.find({
|
||||||
|
projectId: project.id,
|
||||||
|
...(status && { status })
|
||||||
|
});
|
||||||
|
|
||||||
|
return cas;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of certificates for project
|
||||||
|
*/
|
||||||
|
const listProjectCertificates = async ({
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
filter,
|
||||||
|
actor
|
||||||
|
}: TListProjectCertsDTO) => {
|
||||||
|
const project = await projectDAL.findProjectByFilter(filter);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|
||||||
|
const cas = await certificateAuthorityDAL.find({ projectId: project.id });
|
||||||
|
|
||||||
|
const certificates = await certificateDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
caId: cas.map((ca) => ca.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||||
|
);
|
||||||
|
|
||||||
|
const count = await certificateDAL.countCertificatesInProject(project.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificates,
|
||||||
|
totalCount: count
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
@@ -533,6 +619,8 @@ export const projectServiceFactory = ({
|
|||||||
toggleAutoCapitalization,
|
toggleAutoCapitalization,
|
||||||
updateName,
|
updateName,
|
||||||
upgradeProject,
|
upgradeProject,
|
||||||
|
listProjectCas,
|
||||||
|
listProjectCertificates,
|
||||||
updateVersionLimit
|
updateVersionLimit
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { CaStatus } from "../certificate-authority/certificate-authority-types";
|
||||||
|
|
||||||
export enum ProjectFilterType {
|
export enum ProjectFilterType {
|
||||||
ID = "id",
|
ID = "id",
|
||||||
@@ -80,3 +81,14 @@ export type AddUserToWsDTO = {
|
|||||||
userPublicKey: string;
|
userPublicKey: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListProjectCasDTO = {
|
||||||
|
status?: CaStatus;
|
||||||
|
filter: Filter;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListProjectCertsDTO = {
|
||||||
|
filter: Filter;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types";
|
import {
|
||||||
|
TCreatePublicSharedSecretDTO,
|
||||||
|
TCreateSharedSecretDTO,
|
||||||
|
TDeleteSharedSecretDTO,
|
||||||
|
TSharedSecretPermission
|
||||||
|
} from "./secret-sharing-types";
|
||||||
|
|
||||||
type TSecretSharingServiceFactoryDep = {
|
type TSecretSharingServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
@@ -31,6 +36,24 @@ export const secretSharingServiceFactory = ({
|
|||||||
} = createSharedSecretInput;
|
} = createSharedSecretInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
|
||||||
|
if (new Date(expiresAt) < new Date()) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Expiry Time to 1 month
|
||||||
|
const expiryTime = new Date(expiresAt).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
if (expiryTime - currentTime > thirtyDays) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||||
|
if (encryptedValue.length > 13000) {
|
||||||
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
|
}
|
||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
iv,
|
iv,
|
||||||
@@ -44,6 +67,36 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
||||||
|
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput;
|
||||||
|
if (new Date(expiresAt) < new Date()) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Expiry Time to 1 month
|
||||||
|
const expiryTime = new Date(expiresAt).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
if (expiryTime - currentTime > thirtyDays) {
|
||||||
|
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||||
|
if (encryptedValue.length > 13000) {
|
||||||
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt,
|
||||||
|
expiresAfterViews
|
||||||
|
});
|
||||||
|
return { id: newSharedSecret.id };
|
||||||
|
};
|
||||||
|
|
||||||
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@@ -54,6 +107,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||||
|
if (!sharedSecret) return;
|
||||||
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,6 +131,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
createSharedSecret,
|
createSharedSecret,
|
||||||
|
createPublicSharedSecret,
|
||||||
getSharedSecrets,
|
getSharedSecrets,
|
||||||
deleteSharedSecretById,
|
deleteSharedSecretById,
|
||||||
getActiveSharedSecretByIdAndHashedHex
|
getActiveSharedSecretByIdAndHashedHex
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ export type TSharedSecretPermission = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews: number;
|
expiresAfterViews: number;
|
||||||
} & TSharedSecretPermission;
|
};
|
||||||
|
|
||||||
|
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||||
|
|
||||||
export type TDeleteSharedSecretDTO = {
|
export type TDeleteSharedSecretDTO = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
|
|||||||
@@ -2,10 +2,17 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TSecretTagDALFactory } from "./secret-tag-dal";
|
import { TSecretTagDALFactory } from "./secret-tag-dal";
|
||||||
import { TCreateTagDTO, TDeleteTagDTO, TListProjectTagsDTO } from "./secret-tag-types";
|
import {
|
||||||
|
TCreateTagDTO,
|
||||||
|
TDeleteTagDTO,
|
||||||
|
TGetTagByIdDTO,
|
||||||
|
TGetTagBySlugDTO,
|
||||||
|
TListProjectTagsDTO,
|
||||||
|
TUpdateTagDTO
|
||||||
|
} from "./secret-tag-types";
|
||||||
|
|
||||||
type TSecretTagServiceFactoryDep = {
|
type TSecretTagServiceFactoryDep = {
|
||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
@@ -42,11 +49,34 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
|||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
color,
|
color,
|
||||||
createdBy: actorId
|
createdBy: actorId,
|
||||||
|
createdByActorType: actor
|
||||||
});
|
});
|
||||||
return newTag;
|
return newTag;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => {
|
||||||
|
const tag = await secretTagDAL.findById(id);
|
||||||
|
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
|
||||||
|
|
||||||
|
if (slug) {
|
||||||
|
const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId });
|
||||||
|
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
tag.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||||
|
|
||||||
|
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug });
|
||||||
|
return updatedTag;
|
||||||
|
};
|
||||||
|
|
||||||
const deleteTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteTagDTO) => {
|
const deleteTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteTagDTO) => {
|
||||||
const tag = await secretTagDAL.findById(id);
|
const tag = await secretTagDAL.findById(id);
|
||||||
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
|
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
|
||||||
@@ -64,6 +94,38 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
|||||||
return deletedTag;
|
return deletedTag;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTagById = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetTagByIdDTO) => {
|
||||||
|
const tag = await secretTagDAL.findById(id);
|
||||||
|
if (!tag) throw new NotFoundError({ message: "Tag doesn't exist" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
tag.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
|
||||||
|
const tag = await secretTagDAL.findOne({ projectId, slug });
|
||||||
|
if (!tag) throw new NotFoundError({ message: "Tag doesn't exist" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
tag.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
};
|
||||||
|
|
||||||
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
|
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -78,5 +140,5 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
|||||||
return tags;
|
return tags;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { createTag, deleteTag, getProjectTags };
|
return { createTag, deleteTag, getProjectTags, getTagById, getTagBySlug, updateTag };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ export type TCreateTagDTO = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateTagDTO = {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
color?: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetTagByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetTagBySlugDTO = {
|
||||||
|
slug: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteTagDTO = {
|
export type TDeleteTagDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -311,6 +311,40 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findOneWithTags = async (filter: Partial<TSecrets>, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const rawDocs = await (tx || db)(TableName.Secret)
|
||||||
|
.where(filter)
|
||||||
|
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
|
||||||
|
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Secret))
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||||
|
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||||
|
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||||
|
const docs = sqlNestRelationships({
|
||||||
|
data: rawDocs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return docs?.[0];
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindOneWIthTags" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
update,
|
update,
|
||||||
@@ -318,6 +352,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
deleteMany,
|
deleteMany,
|
||||||
bulkUpdateNoVersionIncrement,
|
bulkUpdateNoVersionIncrement,
|
||||||
getSecretTags,
|
getSecretTags,
|
||||||
|
findOneWithTags,
|
||||||
findByFolderId,
|
findByFolderId,
|
||||||
findByFolderIds,
|
findByFolderIds,
|
||||||
findByBlindIndexes,
|
findByBlindIndexes,
|
||||||
|
|||||||
@@ -356,7 +356,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const decryptSecretRaw = (
|
export const decryptSecretRaw = (
|
||||||
secret: TSecrets & { workspace: string; environment: string; secretPath: string },
|
secret: TSecrets & {
|
||||||
|
workspace: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
tags?: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
color?: string | null;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
},
|
||||||
key: string
|
key: string
|
||||||
) => {
|
) => {
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
@@ -396,6 +406,7 @@ export const decryptSecretRaw = (
|
|||||||
_id: secret.id,
|
_id: secret.id,
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
user: secret.userId,
|
user: secret.userId,
|
||||||
|
tags: secret.tags,
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding
|
skipMultilineEncoding: secret.skipMultilineEncoding
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -421,94 +421,88 @@ export const secretQueueFactory = ({
|
|||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
logger.error(new Error("Secret path not found"));
|
throw new Error("Secret path not found");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// start syncing all linked imports also
|
// find all imports made with the given environment and secret path
|
||||||
if (depth < MAX_SYNC_SECRET_DEPTH) {
|
const linkSourceDto = {
|
||||||
// find all imports made with the given environment and secret path
|
projectId,
|
||||||
const linkSourceDto = {
|
importEnv: folder.environment.id,
|
||||||
projectId,
|
importPath: secretPath,
|
||||||
importEnv: folder.environment.id,
|
isReplication: false
|
||||||
importPath: secretPath,
|
};
|
||||||
isReplication: false
|
const imports = await secretImportDAL.find(linkSourceDto);
|
||||||
};
|
|
||||||
const imports = await secretImportDAL.find(linkSourceDto);
|
|
||||||
|
|
||||||
if (imports.length) {
|
if (imports.length) {
|
||||||
// keep calling sync secret for all the imports made
|
// keep calling sync secret for all the imports made
|
||||||
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
|
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||||
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
||||||
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
|
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
|
||||||
logger.info(
|
logger.info(
|
||||||
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
imports
|
imports
|
||||||
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
|
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
|
||||||
// filter out already synced ones
|
// filter out already synced ones
|
||||||
.filter(
|
.filter(
|
||||||
({ folderId }) =>
|
({ folderId }) =>
|
||||||
!deDupeQueue[
|
!deDupeQueue[
|
||||||
uniqueSecretQueueKey(
|
uniqueSecretQueueKey(
|
||||||
foldersGroupedById[folderId][0]?.environmentSlug as string,
|
foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||||
foldersGroupedById[folderId][0]?.path as string
|
foldersGroupedById[folderId][0]?.path as string
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.map(({ folderId }) =>
|
.map(({ folderId }) =>
|
||||||
syncSecrets({
|
syncSecrets({
|
||||||
projectId,
|
projectId,
|
||||||
secretPath: foldersGroupedById[folderId][0]?.path as string,
|
secretPath: foldersGroupedById[folderId][0]?.path as string,
|
||||||
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
|
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||||
_deDupeQueue: deDupeQueue,
|
_deDupeQueue: deDupeQueue,
|
||||||
_depth: depth + 1,
|
_depth: depth + 1,
|
||||||
excludeReplication: true
|
excludeReplication: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretReferences = await secretDAL.findReferencedSecretReferences(
|
const secretReferences = await secretDAL.findReferencedSecretReferences(
|
||||||
projectId,
|
projectId,
|
||||||
folder.environment.slug,
|
folder.environment.slug,
|
||||||
secretPath
|
secretPath
|
||||||
|
);
|
||||||
|
if (secretReferences.length) {
|
||||||
|
const referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||||
|
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
|
||||||
|
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
|
||||||
|
logger.info(
|
||||||
|
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
secretReferences
|
||||||
|
.filter(({ folderId }) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
|
||||||
|
// filter out already synced ones
|
||||||
|
.filter(
|
||||||
|
({ folderId }) =>
|
||||||
|
!deDupeQueue[
|
||||||
|
uniqueSecretQueueKey(
|
||||||
|
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||||
|
referencedFoldersGroupedById[folderId][0]?.path as string
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.map(({ folderId }) =>
|
||||||
|
syncSecrets({
|
||||||
|
projectId,
|
||||||
|
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
|
||||||
|
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||||
|
_deDupeQueue: deDupeQueue,
|
||||||
|
_depth: depth + 1,
|
||||||
|
excludeReplication: true
|
||||||
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
if (secretReferences.length) {
|
|
||||||
const referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
|
|
||||||
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
|
|
||||||
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
|
|
||||||
logger.info(
|
|
||||||
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
secretReferences
|
|
||||||
.filter(({ folderId }) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
|
|
||||||
// filter out already synced ones
|
|
||||||
.filter(
|
|
||||||
({ folderId }) =>
|
|
||||||
!deDupeQueue[
|
|
||||||
uniqueSecretQueueKey(
|
|
||||||
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
|
||||||
referencedFoldersGroupedById[folderId][0]?.path as string
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
.map(({ folderId }) =>
|
|
||||||
syncSecrets({
|
|
||||||
projectId,
|
|
||||||
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
|
|
||||||
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
|
||||||
_deDupeQueue: deDupeQueue,
|
|
||||||
_depth: depth + 1,
|
|
||||||
excludeReplication: true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info(`getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}]`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||||
@@ -550,7 +544,7 @@ export const secretQueueFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await syncIntegrationSecrets({
|
const response = await syncIntegrationSecrets({
|
||||||
createManySecretsRawFn,
|
createManySecretsRawFn,
|
||||||
updateManySecretsRawFn,
|
updateManySecretsRawFn,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
@@ -568,13 +562,15 @@ export const secretQueueFactory = ({
|
|||||||
await integrationDAL.updateById(integration.id, {
|
await integrationDAL.updateById(integration.id, {
|
||||||
lastSyncJobId: job.id,
|
lastSyncJobId: job.id,
|
||||||
lastUsed: new Date(),
|
lastUsed: new Date(),
|
||||||
syncMessage: "",
|
syncMessage: response?.syncMessage ?? "",
|
||||||
isSynced: true
|
isSynced: response?.isSynced ?? true
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err) {
|
||||||
logger.info("Secret integration sync error: %o", err);
|
logger.info("Secret integration sync error: %o", err);
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
err instanceof AxiosError ? JSON.stringify((err as AxiosError)?.response?.data) : (err as Error)?.message;
|
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||||
|
"Unknown error occurred.";
|
||||||
|
|
||||||
await integrationDAL.updateById(integration.id, {
|
await integrationDAL.updateById(integration.id, {
|
||||||
lastSyncJobId: job.id,
|
lastSyncJobId: job.id,
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ export const secretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const secret = await (version === undefined
|
const secret = await (version === undefined
|
||||||
? secretDAL.findOne({
|
? secretDAL.findOneWithTags({
|
||||||
folderId,
|
folderId,
|
||||||
type: secretType,
|
type: secretType,
|
||||||
userId: secretType === SecretType.Personal ? actorId : null,
|
userId: secretType === SecretType.Personal ? actorId : null,
|
||||||
@@ -1120,7 +1120,8 @@ export const secretServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
secretValue,
|
secretValue,
|
||||||
secretComment,
|
secretComment,
|
||||||
skipMultilineEncoding
|
skipMultilineEncoding,
|
||||||
|
tagIds
|
||||||
}: TCreateSecretRawDTO) => {
|
}: TCreateSecretRawDTO) => {
|
||||||
const botKey = await projectBotService.getBotKey(projectId);
|
const botKey = await projectBotService.getBotKey(projectId);
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
@@ -1148,7 +1149,8 @@ export const secretServiceFactory = ({
|
|||||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
secretCommentIV: secretCommentEncrypted.iv,
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
secretCommentTag: secretCommentEncrypted.tag,
|
secretCommentTag: secretCommentEncrypted.tag,
|
||||||
skipMultilineEncoding
|
skipMultilineEncoding,
|
||||||
|
tags: tagIds
|
||||||
});
|
});
|
||||||
|
|
||||||
return decryptSecretRaw(secret, botKey);
|
return decryptSecretRaw(secret, botKey);
|
||||||
@@ -1165,7 +1167,8 @@ export const secretServiceFactory = ({
|
|||||||
type,
|
type,
|
||||||
secretPath,
|
secretPath,
|
||||||
secretValue,
|
secretValue,
|
||||||
skipMultilineEncoding
|
skipMultilineEncoding,
|
||||||
|
tagIds
|
||||||
}: TUpdateSecretRawDTO) => {
|
}: TUpdateSecretRawDTO) => {
|
||||||
const botKey = await projectBotService.getBotKey(projectId);
|
const botKey = await projectBotService.getBotKey(projectId);
|
||||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||||
@@ -1185,7 +1188,8 @@ export const secretServiceFactory = ({
|
|||||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
secretValueIV: secretValueEncrypted.iv,
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
secretValueTag: secretValueEncrypted.tag,
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
skipMultilineEncoding
|
skipMultilineEncoding,
|
||||||
|
tags: tagIds
|
||||||
});
|
});
|
||||||
|
|
||||||
await snapshotService.performSnapshot(secret.folderId);
|
await snapshotService.performSnapshot(secret.folderId);
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export type TCreateSecretRawDTO = TProjectPermission & {
|
|||||||
secretName: string;
|
secretName: string;
|
||||||
secretValue: string;
|
secretValue: string;
|
||||||
type: SecretType;
|
type: SecretType;
|
||||||
|
tagIds?: string[];
|
||||||
secretComment?: string;
|
secretComment?: string;
|
||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
};
|
};
|
||||||
@@ -174,6 +175,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & {
|
|||||||
secretName: string;
|
secretName: string;
|
||||||
secretValue?: string;
|
secretValue?: string;
|
||||||
type: SecretType;
|
type: SecretType;
|
||||||
|
tagIds?: string[];
|
||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
secretReminderRepeatDays?: number | null;
|
secretReminderRepeatDays?: number | null;
|
||||||
secretReminderNote?: string | null;
|
secretReminderNote?: string | null;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve certificate / chain"
|
||||||
|
openapi: "GET /api/v1/pki/ca/{caId}/certificate"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/pki/ca"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve CRL"
|
||||||
|
openapi: "GET /api/v1/pki/ca/{caId}/crl"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get CSR"
|
||||||
|
openapi: "GET /api/v1/pki/ca/{caId}/csr"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/pki/ca/{caId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Import certificate"
|
||||||
|
openapi: "POST /api/v1/pki/ca/{caId}/import-certificate"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Issue certificate"
|
||||||
|
openapi: "POST /api/v1/pki/ca/{caId}/issue-certificate"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve"
|
||||||
|
openapi: "GET /api/v1/pki/ca/{caId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sign intermediate certificate"
|
||||||
|
openapi: "POST /api/v1/pki/ca/{caId}/sign-intermediate"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/pki/ca/{caId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Certificate Body / Chain"
|
||||||
|
openapi: "GET /api/v1/pki/certificates/{serialNumber}/certificate"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/pki/certificates/{serialNumber}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve"
|
||||||
|
openapi: "GET /api/v1/pki/certificates/{serialNumber}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Revoke"
|
||||||
|
openapi: "POST /api/v1/pki/certificates/{serialNumber}/revoke"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get By ID"
|
||||||
|
openapi: "GET /api/v1/workspace/{projectId}/tags/{tagId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get By Slug"
|
||||||
|
openapi: "GET /api/v1/workspace/{projectId}/tags/slug/{tagSlug}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/workspace/{projectId}/tags/{tagId}"
|
||||||
|
---
|
||||||
@@ -4,6 +4,25 @@ title: "Changelog"
|
|||||||
|
|
||||||
The changelog below reflects new product developments and updates on a monthly basis.
|
The changelog below reflects new product developments and updates on a monthly basis.
|
||||||
|
|
||||||
|
## May 2024
|
||||||
|
- Released [AWS](https://infisical.com/docs/documentation/platform/identities/aws-auth), [GCP](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure](https://infisical.com/docs/documentation/platform/identities/azure-auth), and [Kubernetes](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth) Native Auth Methods.
|
||||||
|
- Added [Secret Sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality for sharing sensitive data through encrypted links – within and outside of an organization.
|
||||||
|
- Updated [Secret Referencing](https://infisical.com/docs/documentation/platform/secret-reference) to be supported in all Infisical clients. Infisical UI is now able to provide automatic reference suggestions when typing.
|
||||||
|
- Released new [Infisical Jenkins Plugin](https://infisical.com/docs/integrations/cicd/jenkins).
|
||||||
|
- Added statuses and manual sync option to integrations in the Dashboard UI.
|
||||||
|
- Released universal [Audit Log Streaming](https://infisical.com/docs/documentation/platform/audit-log-streams).
|
||||||
|
- Added [Dynamic Secret template for AWS IAM](https://infisical.com/docs/documentation/platform/dynamic-secrets/aws-iam).
|
||||||
|
- Added support for syncing tags and custom KMS keys to [AWS Secrets Manager](https://infisical.com/docs/integrations/cloud/aws-secret-manager) and [Parameter Store](https://infisical.com/docs/integrations/cloud/aws-parameter-store) Integrations.
|
||||||
|
- Officially released Infisical on [AWS Marketplace](https://infisical.com/blog/infisical-launches-on-aws-marketplace).
|
||||||
|
|
||||||
|
## April 2024
|
||||||
|
- Added [Access Requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests) as part of self-serve secrets management workflows.
|
||||||
|
- Added [Temporary Access Provisioning](https://infisical.com/docs/documentation/platform/access-controls/temporary-access) for roles and additional privileges.
|
||||||
|
|
||||||
|
## May 2024
|
||||||
|
- Released support for [Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview).
|
||||||
|
- Released the concept of [Additional Privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges) on top of user/machine roles.
|
||||||
|
|
||||||
## Feb 2024
|
## Feb 2024
|
||||||
- Added org-scoped authentication enforcement for SAML
|
- Added org-scoped authentication enforcement for SAML
|
||||||
- Added support for [SCIM](https://infisical.com/docs/documentation/platform/scim/overview) along with instructions for setting it up with [Okta](https://infisical.com/docs/documentation/platform/scim/okta), [Azure](https://infisical.com/docs/documentation/platform/scim/azure), and [JumpCloud](https://infisical.com/docs/documentation/platform/scim/jumpcloud).
|
- Added support for [SCIM](https://infisical.com/docs/documentation/platform/scim/overview) along with instructions for setting it up with [Okta](https://infisical.com/docs/documentation/platform/scim/okta), [Azure](https://infisical.com/docs/documentation/platform/scim/azure), and [JumpCloud](https://infisical.com/docs/documentation/platform/scim/jumpcloud).
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ There is a number of issues that arise with secret management in local developme
|
|||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
One of the main benefits of Infisical is the facilitation of secret management workflows in local development use cases. In particular, Infisical heavily follows the "Security Shift Left" principle to enable developers to effotlessly follow secure practices when coding.
|
One of the main benefits of Infisical is the facilitation of secret management workflows in local development use cases. In particular, Infisical heavily follows the "Security Shift Left" principle to enable developers to effortlessly follow secure practices when coding.
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
[Infisical CLI](/cli/overview) is the most frequently used Infisical tool for secret management in local development environments. It makes it easy to inject secrets right into the local application environments based on the permissions given to corresponsing developers.
|
[Infisical CLI](/cli/overview) is the most frequently used Infisical tool for secret management in local development environments. It makes it easy to inject secrets right into the local application environments based on the permissions given to corresponding developers.
|
||||||
|
|
||||||
### Dashboard
|
### Dashboard
|
||||||
|
|
||||||
@@ -31,4 +31,4 @@ By default, all the secrets in the Infisical environments are shared among proje
|
|||||||
|
|
||||||
### Secret Scanning
|
### Secret Scanning
|
||||||
|
|
||||||
In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionlality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub.
|
In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user