mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 05:29:16 +00:00
Merge remote-tracking branch 'origin/main' into feat/blockDuplicateSyncDestination
This commit is contained in:
@@ -1,57 +0,0 @@
|
|||||||
## @section Common parameters
|
|
||||||
##
|
|
||||||
|
|
||||||
## @param nameOverride Override release name
|
|
||||||
##
|
|
||||||
nameOverride: ""
|
|
||||||
## @param fullnameOverride Override release fullname
|
|
||||||
##
|
|
||||||
fullnameOverride: ""
|
|
||||||
|
|
||||||
## @section Infisical backend parameters
|
|
||||||
## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes
|
|
||||||
##
|
|
||||||
|
|
||||||
infisical:
|
|
||||||
autoDatabaseSchemaMigration: false
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
name: infisical
|
|
||||||
replicaCount: 3
|
|
||||||
image:
|
|
||||||
repository: infisical/staging_infisical
|
|
||||||
tag: "latest"
|
|
||||||
pullPolicy: Always
|
|
||||||
|
|
||||||
deploymentAnnotations:
|
|
||||||
secrets.infisical.com/auto-reload: "true"
|
|
||||||
|
|
||||||
kubeSecretRef: "managed-secret"
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
## @param ingress.enabled Enable ingress
|
|
||||||
##
|
|
||||||
enabled: true
|
|
||||||
## @param ingress.ingressClassName Ingress class name
|
|
||||||
##
|
|
||||||
ingressClassName: nginx
|
|
||||||
## @param ingress.nginx.enabled Ingress controller
|
|
||||||
##
|
|
||||||
# nginx:
|
|
||||||
# enabled: true
|
|
||||||
## @param ingress.annotations Ingress annotations
|
|
||||||
##
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
hostName: "gamma.infisical.com"
|
|
||||||
tls:
|
|
||||||
- secretName: letsencrypt-prod
|
|
||||||
hosts:
|
|
||||||
- gamma.infisical.com
|
|
||||||
|
|
||||||
postgresql:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
redis:
|
|
||||||
enabled: false
|
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
--config ct.yaml \
|
--config ct.yaml \
|
||||||
--charts helm-charts/infisical-standalone-postgres \
|
--charts helm-charts/infisical-standalone-postgres \
|
||||||
--helm-extra-args="--timeout=300s" \
|
--helm-extra-args="--timeout=300s" \
|
||||||
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \
|
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.151.0" \
|
||||||
--namespace infisical-standalone-postgres
|
--namespace infisical-standalone-postgres
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up chart-testing
|
- name: Set up chart-testing
|
||||||
uses: helm/[email protected]
|
uses: helm/[email protected]
|
||||||
|
with:
|
||||||
|
yamale_version: "6.0.0"
|
||||||
|
|
||||||
- name: Run chart-testing (lint)
|
- name: Run chart-testing (lint)
|
||||||
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up chart-testing
|
- name: Set up chart-testing
|
||||||
uses: helm/[email protected]
|
uses: helm/[email protected]
|
||||||
|
with:
|
||||||
|
yamale_version: "6.0.0"
|
||||||
|
|
||||||
- name: Run chart-testing (lint)
|
- name: Run chart-testing (lint)
|
||||||
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
||||||
|
|||||||
@@ -66,5 +66,5 @@ jobs:
|
|||||||
--config ct.yaml \
|
--config ct.yaml \
|
||||||
--charts helm-charts/infisical-standalone-postgres \
|
--charts helm-charts/infisical-standalone-postgres \
|
||||||
--helm-extra-args="--timeout=300s" \
|
--helm-extra-args="--timeout=300s" \
|
||||||
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres --set infisical.autoBootstrap.enabled=true" \
|
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.151.0 --set infisical.autoBootstrap.enabled=true" \
|
||||||
--namespace infisical-standalone-postgres
|
--namespace infisical-standalone-postgres
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
|||||||
|
|
||||||
# Install Infisical CLI
|
# Install Infisical CLI
|
||||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||||
&& apt-get update && apt-get install -y infisical=0.42.6 \
|
&& apt-get update && apt-get install -y infisical=0.43.14 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
|
|
||||||
# Install Infisical CLI
|
# Install Infisical CLI
|
||||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||||
&& apt-get update && apt-get install -y infisical=0.42.6 \
|
&& apt-get update && apt-get install -y infisical=0.43.14 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ COPY --from=build /app .
|
|||||||
# Install Infisical CLI
|
# Install Infisical CLI
|
||||||
RUN apt-get install -y curl bash && \
|
RUN apt-get install -y curl bash && \
|
||||||
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||||
apt-get update && apt-get install -y infisical=0.41.89 git
|
apt-get update && apt-get install -y infisical=0.43.14 git
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||||
CMD node healthcheck.js
|
CMD node healthcheck.js
|
||||||
|
|||||||
@@ -49,25 +49,26 @@ RUN rm -fr ${SOFTHSM2_SOURCES}
|
|||||||
# Install pkcs11-tool
|
# Install pkcs11-tool
|
||||||
RUN apt-get install -y opensc
|
RUN apt-get install -y opensc
|
||||||
|
|
||||||
RUN mkdir -p /etc/softhsm2/tokens && \
|
|
||||||
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
|
||||||
|
|
||||||
# ? App setup
|
# ? App setup
|
||||||
|
|
||||||
# Install Infisical CLI
|
# Install Infisical CLI
|
||||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y infisical=0.41.89
|
apt-get install -y infisical=0.43.14
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package.json
|
COPY package.json package.json
|
||||||
COPY package-lock.json package-lock.json
|
COPY package-lock.json package-lock.json
|
||||||
|
|
||||||
|
COPY dev-entrypoint.sh dev-entrypoint.sh
|
||||||
|
RUN chmod +x dev-entrypoint.sh
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/dev-entrypoint.sh"]
|
||||||
CMD ["npm", "run", "dev:docker"]
|
CMD ["npm", "run", "dev:docker"]
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ RUN rm -fr ${SOFTHSM2_SOURCES}
|
|||||||
# Install pkcs11-tool
|
# Install pkcs11-tool
|
||||||
RUN apt-get install -y opensc
|
RUN apt-get install -y opensc
|
||||||
|
|
||||||
RUN mkdir -p /etc/softhsm2/tokens && \
|
|
||||||
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
|
||||||
|
|
||||||
WORKDIR /openssl-build
|
WORKDIR /openssl-build
|
||||||
RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
||||||
&& tar -xf openssl-3.1.2.tar.gz \
|
&& tar -xf openssl-3.1.2.tar.gz \
|
||||||
@@ -70,13 +67,16 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
|||||||
# Install Infisical CLI
|
# Install Infisical CLI
|
||||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y infisical=0.41.89
|
apt-get install -y infisical=0.43.14
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package.json
|
COPY package.json package.json
|
||||||
COPY package-lock.json package-lock.json
|
COPY package-lock.json package-lock.json
|
||||||
|
|
||||||
|
COPY dev-entrypoint.sh dev-entrypoint.sh
|
||||||
|
RUN chmod +x dev-entrypoint.sh
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -87,4 +87,5 @@ ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
|
|||||||
# ENV NODE_OPTIONS=--force-fips # Note(Daniel): We can't set this on the node options because it may break for existing folks using the infisical/infisical-fips image. Instead we call crypto.setFips(true) at runtime.
|
# ENV NODE_OPTIONS=--force-fips # Note(Daniel): We can't set this on the node options because it may break for existing folks using the infisical/infisical-fips image. Instead we call crypto.setFips(true) at runtime.
|
||||||
ENV FIPS_ENABLED=true
|
ENV FIPS_ENABLED=true
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/dev-entrypoint.sh"]
|
||||||
CMD ["npm", "run", "dev:docker"]
|
CMD ["npm", "run", "dev:docker"]
|
||||||
|
|||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
update-ca-certificates
|
||||||
|
|
||||||
|
# Initialize SoftHSM token if it doesn't exist
|
||||||
|
if [ ! -f /etc/softhsm2/tokens/auth-app.db ]; then
|
||||||
|
echo "Initializing SoftHSM token..."
|
||||||
|
mkdir -p /etc/softhsm2/tokens
|
||||||
|
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||||
|
echo "SoftHSM token initialized"
|
||||||
|
else
|
||||||
|
echo "SoftHSM token already exists, skipping initialization"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -146,7 +146,8 @@ describe("Service token secret ops", async () => {
|
|||||||
let folderId = "";
|
let folderId = "";
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
initLogger();
|
initLogger();
|
||||||
await initEnvConfig(testSuperAdminDAL, logger);
|
|
||||||
|
await initEnvConfig(testHsmService, testKmsRootConfigDAL, testSuperAdminDAL, logger);
|
||||||
|
|
||||||
serviceToken = await createServiceToken(
|
serviceToken = await createServiceToken(
|
||||||
[{ secretPath: "/**", environment: seedData1.environment.slug }],
|
[{ secretPath: "/**", environment: seedData1.environment.slug }],
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ describe("Secret V3 Router", async () => {
|
|||||||
let folderId = "";
|
let folderId = "";
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
initLogger();
|
initLogger();
|
||||||
await initEnvConfig(testSuperAdminDAL, logger);
|
await initEnvConfig(testHsmService, testKmsRootConfigDAL, testSuperAdminDAL, logger);
|
||||||
|
|
||||||
const projectKeyRes = await testServer.inject({
|
const projectKeyRes = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { crypto } from "@app/lib/crypto/cryptography";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { seedData1 } from "@app/db/seed-data";
|
import { seedData1 } from "@app/db/seed-data";
|
||||||
import { getDatabaseCredentials, initEnvConfig } from "@app/lib/config/env";
|
import { getDatabaseCredentials, getHsmConfig, initEnvConfig } from "@app/lib/config/env";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
import { main } from "@app/server/app";
|
import { main } from "@app/server/app";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
@@ -20,6 +20,8 @@ import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
|||||||
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
import { buildRedisFromConfig } from "@app/lib/config/redis";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
import { bootstrapCheck } from "@app/server/boot-strap-check";
|
import { bootstrapCheck } from "@app/server/boot-strap-check";
|
||||||
|
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
|
|
||||||
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
||||||
export default {
|
export default {
|
||||||
@@ -28,6 +30,7 @@ export default {
|
|||||||
async setup() {
|
async setup() {
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
const databaseCredentials = getDatabaseCredentials(logger);
|
const databaseCredentials = getDatabaseCredentials(logger);
|
||||||
|
const hsmConfig = getHsmConfig(logger);
|
||||||
|
|
||||||
const db = initDbConnection({
|
const db = initDbConnection({
|
||||||
dbConnectionUri: databaseCredentials.dbConnectionUri,
|
dbConnectionUri: databaseCredentials.dbConnectionUri,
|
||||||
@@ -35,7 +38,19 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(db);
|
const superAdminDAL = superAdminDALFactory(db);
|
||||||
const envCfg = await initEnvConfig(superAdminDAL, logger);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
|
|
||||||
|
const hsmModule = initializeHsmModule(hsmConfig);
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
hsmModule: hsmModule.getModule(),
|
||||||
|
envConfig: hsmConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
await hsmService.startService();
|
||||||
|
|
||||||
|
const envCfg = await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger);
|
||||||
|
|
||||||
const redis = buildRedisFromConfig(envCfg);
|
const redis = buildRedisFromConfig(envCfg);
|
||||||
await redis.flushdb("SYNC");
|
await redis.flushdb("SYNC");
|
||||||
@@ -68,16 +83,14 @@ export default {
|
|||||||
|
|
||||||
await queue.initialize();
|
await queue.initialize();
|
||||||
|
|
||||||
const hsmModule = initializeHsmModule(envCfg);
|
|
||||||
hsmModule.initialize();
|
|
||||||
|
|
||||||
const server = await main({
|
const server = await main({
|
||||||
db,
|
db,
|
||||||
smtp,
|
smtp,
|
||||||
logger,
|
logger,
|
||||||
queue,
|
queue,
|
||||||
keyStore,
|
keyStore,
|
||||||
hsmModule: hsmModule.getModule(),
|
hsmService,
|
||||||
|
kmsRootConfigDAL,
|
||||||
superAdminDAL,
|
superAdminDAL,
|
||||||
redis,
|
redis,
|
||||||
envConfig: envCfg
|
envConfig: envCfg
|
||||||
@@ -92,6 +105,10 @@ export default {
|
|||||||
// @ts-expect-error type
|
// @ts-expect-error type
|
||||||
globalThis.testSuperAdminDAL = superAdminDAL;
|
globalThis.testSuperAdminDAL = superAdminDAL;
|
||||||
// @ts-expect-error type
|
// @ts-expect-error type
|
||||||
|
globalThis.testKmsRootConfigDAL = kmsRootConfigDAL;
|
||||||
|
// @ts-expect-error type
|
||||||
|
globalThis.testHsmService = hsmService;
|
||||||
|
// @ts-expect-error type
|
||||||
globalThis.jwtAuthToken = crypto.jwt().sign(
|
globalThis.jwtAuthToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||||
|
|||||||
Generated
+1032
-2040
File diff suppressed because it is too large
Load Diff
@@ -40,10 +40,10 @@
|
|||||||
"type:check": "node --max-old-space-size=8192 ./node_modules/.bin/tsc --noEmit",
|
"type:check": "node --max-old-space-size=8192 ./node_modules/.bin/tsc --noEmit",
|
||||||
"lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
|
"lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
|
||||||
"lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
|
"lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
|
||||||
"test:unit": "vitest run -c vitest.unit.config.ts",
|
"test:unit": "vitest run -c vitest.unit.config.mts",
|
||||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
"test:e2e": "vitest run -c vitest.e2e.config.mts --bail=1",
|
||||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
"test:e2e-watch": "vitest -c vitest.e2e.config.mts --bail=1",
|
||||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.mts",
|
||||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
|
"generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
|
||||||
"auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest",
|
"auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"@types/jsrp": "^0.2.6",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.17.30",
|
"@types/node": "^20.19.0",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
@@ -130,10 +130,10 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"tsup": "^8.0.1",
|
"tsup": "^8.5.0",
|
||||||
"tsx": "^4.4.0",
|
"tsx": "^4.4.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vitest": "^1.2.2"
|
"vitest": "^3.0.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||||
|
|||||||
Vendored
+4
@@ -1,7 +1,9 @@
|
|||||||
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
|
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
|
||||||
|
|
||||||
|
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
import { CustomLogger } from "@app/lib/logger/logger";
|
import { CustomLogger } from "@app/lib/logger/logger";
|
||||||
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
|
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
|
||||||
|
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -16,5 +18,7 @@ declare global {
|
|||||||
// used only for testing
|
// used only for testing
|
||||||
const testServer: FastifyZodProvider;
|
const testServer: FastifyZodProvider;
|
||||||
const testSuperAdminDAL: TSuperAdminDALFactory;
|
const testSuperAdminDAL: TSuperAdminDALFactory;
|
||||||
|
const testKmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||||
|
const testHsmService: THsmServiceFactory;
|
||||||
const jwtAuthToken: string;
|
const jwtAuthToken: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { Knex } from "knex";
|
|||||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName } from "../schemas";
|
import { SecretKeyEncoding, TableName } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { createCircularCache } from "./utils/ring-buffer";
|
import { createCircularCache } from "./utils/ring-buffer";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
const BATCH_SIZE = 500;
|
const BATCH_SIZE = 500;
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
@@ -25,10 +26,12 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (hasUrl) t.string("url").nullable().alter();
|
if (hasUrl) t.string("url").nullable().alter();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
|
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
|
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory";
|
|||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName } from "../schemas";
|
import { SecretKeyEncoding, TableName } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { createCircularCache } from "./utils/ring-buffer";
|
import { createCircularCache } from "./utils/ring-buffer";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
const BATCH_SIZE = 500;
|
const BATCH_SIZE = 500;
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
@@ -30,8 +31,12 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
|
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
|
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory";
|
|||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName } from "../schemas";
|
import { SecretKeyEncoding, TableName } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { createCircularCache } from "./utils/ring-buffer";
|
import { createCircularCache } from "./utils/ring-buffer";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
const BATCH_SIZE = 500;
|
const BATCH_SIZE = 500;
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
@@ -24,8 +25,11 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
|
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory";
|
|||||||
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas";
|
import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { createCircularCache } from "./utils/ring-buffer";
|
import { createCircularCache } from "./utils/ring-buffer";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
const BATCH_SIZE = 500;
|
const BATCH_SIZE = 500;
|
||||||
const reencryptIdentityK8sAuth = async (knex: Knex) => {
|
const reencryptIdentityK8sAuth = async (knex: Knex) => {
|
||||||
@@ -55,9 +56,11 @@ const reencryptIdentityK8sAuth = async (knex: Knex) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
|
||||||
|
|
||||||
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
const orgEncryptionRingBuffer =
|
const orgEncryptionRingBuffer =
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { inMemoryKeyStore } from "@app/keystore/memory";
|
|||||||
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas";
|
import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { createCircularCache } from "./utils/ring-buffer";
|
import { createCircularCache } from "./utils/ring-buffer";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
const BATCH_SIZE = 500;
|
const BATCH_SIZE = 500;
|
||||||
const reencryptIdentityOidcAuth = async (knex: Knex) => {
|
const reencryptIdentityOidcAuth = async (knex: Knex) => {
|
||||||
@@ -35,8 +36,11 @@ const reencryptIdentityOidcAuth = async (knex: Knex) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
|
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import { inMemoryKeyStore } from "@app/keystore/memory";
|
|||||||
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName } from "../schemas";
|
import { SecretKeyEncoding, TableName } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { createCircularCache } from "./utils/ring-buffer";
|
import { createCircularCache } from "./utils/ring-buffer";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
const BATCH_SIZE = 500;
|
const BATCH_SIZE = 500;
|
||||||
const reencryptSamlConfig = async (knex: Knex) => {
|
const reencryptSamlConfig = async (knex: Knex, kmsService: TKmsServiceFactory) => {
|
||||||
const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint");
|
const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint");
|
||||||
const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer");
|
const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer");
|
||||||
const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate");
|
const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate");
|
||||||
@@ -28,10 +30,6 @@ const reencryptSamlConfig = async (knex: Knex) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
|
||||||
const keyStore = inMemoryKeyStore();
|
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
|
||||||
const orgEncryptionRingBuffer =
|
const orgEncryptionRingBuffer =
|
||||||
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
||||||
|
|
||||||
@@ -159,7 +157,7 @@ const reencryptSamlConfig = async (knex: Knex) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reencryptLdapConfig = async (knex: Knex) => {
|
const reencryptLdapConfig = async (knex: Knex, kmsService: TKmsServiceFactory) => {
|
||||||
const hasEncryptedLdapBindDNColum = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN");
|
const hasEncryptedLdapBindDNColum = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN");
|
||||||
const hasEncryptedLdapBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass");
|
const hasEncryptedLdapBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass");
|
||||||
const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate");
|
const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate");
|
||||||
@@ -194,10 +192,6 @@ const reencryptLdapConfig = async (knex: Knex) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
|
||||||
const keyStore = inMemoryKeyStore();
|
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
|
||||||
const orgEncryptionRingBuffer =
|
const orgEncryptionRingBuffer =
|
||||||
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
||||||
|
|
||||||
@@ -323,7 +317,7 @@ const reencryptLdapConfig = async (knex: Knex) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reencryptOidcConfig = async (knex: Knex) => {
|
const reencryptOidcConfig = async (knex: Knex, kmsService: TKmsServiceFactory) => {
|
||||||
const hasEncryptedOidcClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId");
|
const hasEncryptedOidcClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId");
|
||||||
const hasEncryptedOidcClientSecretColumn = await knex.schema.hasColumn(
|
const hasEncryptedOidcClientSecretColumn = await knex.schema.hasColumn(
|
||||||
TableName.OidcConfig,
|
TableName.OidcConfig,
|
||||||
@@ -354,10 +348,6 @@ const reencryptOidcConfig = async (knex: Knex) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
|
||||||
const keyStore = inMemoryKeyStore();
|
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
|
||||||
const orgEncryptionRingBuffer =
|
const orgEncryptionRingBuffer =
|
||||||
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
createCircularCache<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
||||||
|
|
||||||
@@ -462,9 +452,18 @@ const reencryptOidcConfig = async (knex: Knex) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
await reencryptSamlConfig(knex);
|
initLogger();
|
||||||
await reencryptLdapConfig(knex);
|
|
||||||
await reencryptOidcConfig(knex);
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
|
const keyStore = inMemoryKeyStore();
|
||||||
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|
||||||
|
await reencryptSamlConfig(knex, kmsService);
|
||||||
|
await reencryptLdapConfig(knex, kmsService);
|
||||||
|
await reencryptOidcConfig(knex, kmsService);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropSamlConfigColumns = async (knex: Knex) => {
|
const dropSamlConfigColumns = async (knex: Knex) => {
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { Knex } from "knex";
|
|||||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
import { initLogger } from "@app/lib/logger";
|
import { initLogger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
|
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
|
||||||
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
|
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
|
||||||
@@ -40,8 +41,10 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,23 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||||
import { selectAllTableCols } from "@app/lib/knex";
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
export async function up(knex: Knex) {
|
export async function up(knex: Knex) {
|
||||||
const existingSuperAdminsWithGithubConnection = await knex(TableName.SuperAdmin)
|
const existingSuperAdminsWithGithubConnection = await knex(TableName.SuperAdmin)
|
||||||
.select(selectAllTableCols(TableName.SuperAdmin))
|
.select(selectAllTableCols(TableName.SuperAdmin))
|
||||||
.whereNotNull(`${TableName.SuperAdmin}.encryptedGitHubAppConnectionClientId`);
|
.whereNotNull(`${TableName.SuperAdmin}.encryptedGitHubAppConnectionClientId`);
|
||||||
|
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TableName } from "../schemas";
|
import { SecretKeyEncoding, TableName } from "../schemas";
|
||||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
import { getMigrationEnvConfig, getMigrationHsmConfig } from "./utils/env-config";
|
||||||
import { createCircularCache } from "./utils/ring-buffer";
|
import { createCircularCache } from "./utils/ring-buffer";
|
||||||
import { getMigrationEncryptionServices } from "./utils/services";
|
import { getMigrationEncryptionServices, getMigrationHsmService } from "./utils/services";
|
||||||
|
|
||||||
const BATCH_SIZE = 500;
|
const BATCH_SIZE = 500;
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
@@ -25,8 +26,10 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!hasEncryptedCredentials) {
|
if (!hasEncryptedCredentials) {
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
|
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
@@ -131,8 +134,11 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials");
|
const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials");
|
||||||
|
|
||||||
if (hasEncryptedCredentials) {
|
if (hasEncryptedCredentials) {
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig: getMigrationHsmConfig() });
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const envConfig = await getMigrationEnvConfig(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
const keyStore = inMemoryKeyStore();
|
const keyStore = inMemoryKeyStore();
|
||||||
|
|
||||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled"))) {
|
||||||
|
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||||
|
t.boolean("rotationEnabled").notNullable().defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds"))) {
|
||||||
|
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||||
|
t.integer("rotationIntervalSeconds").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt"))) {
|
||||||
|
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||||
|
t.timestamp("lastRotatedAt").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials"))) {
|
||||||
|
await knex.schema.alterTable(TableName.PamResource, (t) => {
|
||||||
|
t.binary("encryptedRotationAccountCredentials").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.PamResource, "encryptedRotationAccountCredentials")) {
|
||||||
|
await knex.schema.alterTable(TableName.PamResource, (t) => {
|
||||||
|
t.dropColumn("encryptedRotationAccountCredentials");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasColumn(TableName.PamAccount, "rotationEnabled")) {
|
||||||
|
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||||
|
t.dropColumn("rotationEnabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasColumn(TableName.PamAccount, "rotationIntervalSeconds")) {
|
||||||
|
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||||
|
t.dropColumn("rotationIntervalSeconds");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (await knex.schema.hasColumn(TableName.PamAccount, "lastRotatedAt")) {
|
||||||
|
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||||
|
t.dropColumn("lastRotatedAt");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,8 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
[TableName.Identity, TableName.Membership, AccessScope.Organization]
|
[TableName.Identity, TableName.Membership, AccessScope.Organization]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await knex.raw(`DELETE FROM ?? WHERE "orgId" IS NULL`, [TableName.Identity]);
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||||
t.uuid("orgId").notNullable().alter();
|
t.uuid("orgId").notNullable().alter();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "autoRenewDays")) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
||||||
|
t.renameColumn("autoRenewDays", "renewBeforeDays");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays"))) {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.integer("renewBeforeDays").nullable();
|
||||||
|
t.uuid("renewedFromCertificateId").nullable();
|
||||||
|
t.uuid("renewedByCertificateId").nullable();
|
||||||
|
t.text("renewalError").nullable();
|
||||||
|
t.string("keyAlgorithm").nullable();
|
||||||
|
t.string("signatureAlgorithm").nullable();
|
||||||
|
t.foreign("renewedFromCertificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
|
||||||
|
t.foreign("renewedByCertificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
|
||||||
|
t.index("renewedFromCertificateId");
|
||||||
|
t.index("renewedByCertificateId");
|
||||||
|
t.index("renewBeforeDays");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays")) {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.dropForeign(["renewedFromCertificateId"]);
|
||||||
|
t.dropForeign(["renewedByCertificateId"]);
|
||||||
|
t.dropIndex("renewedFromCertificateId");
|
||||||
|
t.dropIndex("renewedByCertificateId");
|
||||||
|
t.dropIndex("renewBeforeDays");
|
||||||
|
t.dropColumn("renewBeforeDays");
|
||||||
|
t.dropColumn("renewedFromCertificateId");
|
||||||
|
t.dropColumn("renewedByCertificateId");
|
||||||
|
t.dropColumn("renewalError");
|
||||||
|
t.dropColumn("keyAlgorithm");
|
||||||
|
t.dropColumn("signatureAlgorithm");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "renewBeforeDays")) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
||||||
|
t.renameColumn("renewBeforeDays", "autoRenewDays");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { AccessScope, TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasGroupsTable = await knex.schema.hasTable(TableName.Groups);
|
||||||
|
const hasMembershipTable = await knex.schema.hasTable(TableName.Membership);
|
||||||
|
const hasMembershipRoleTable = await knex.schema.hasTable(TableName.MembershipRole);
|
||||||
|
|
||||||
|
if (!hasGroupsTable || !hasMembershipTable || !hasMembershipRoleTable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupsWithoutMembership = await knex
|
||||||
|
.select(
|
||||||
|
`${TableName.Groups}.id`,
|
||||||
|
`${TableName.Groups}.orgId`,
|
||||||
|
`${TableName.Groups}.role`,
|
||||||
|
`${TableName.Groups}.roleId`
|
||||||
|
)
|
||||||
|
.from(TableName.Groups)
|
||||||
|
.leftJoin(TableName.Membership, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`)
|
||||||
|
.whereNull(`${TableName.Membership}.actorGroupId`);
|
||||||
|
|
||||||
|
if (groupsWithoutMembership.length > 0) {
|
||||||
|
const membershipInserts = groupsWithoutMembership.map((group) => ({
|
||||||
|
actorGroupId: group.id,
|
||||||
|
scope: AccessScope.Organization,
|
||||||
|
scopeOrgId: group.orgId,
|
||||||
|
isActive: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const insertedMemberships = await knex(TableName.Membership).insert(membershipInserts).returning("*");
|
||||||
|
|
||||||
|
const membershipRoleInserts = insertedMemberships.map((membership, index) => {
|
||||||
|
const group = groupsWithoutMembership[index];
|
||||||
|
return {
|
||||||
|
membershipId: membership.id,
|
||||||
|
role: group.role,
|
||||||
|
customRoleId: group.roleId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex(TableName.MembershipRole).insert(membershipRoleInserts);
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Membership, (t) => {
|
||||||
|
t.check(
|
||||||
|
`("actorUserId" IS NOT NULL OR "actorIdentityId" IS NOT NULL OR "actorGroupId" IS NOT NULL)`,
|
||||||
|
undefined,
|
||||||
|
"at_least_one_actor"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Membership, (t) => {
|
||||||
|
t.dropChecks("at_least_one_actor");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { zpStr } from "@app/lib/zod";
|
import { zpStr } from "@app/lib/zod";
|
||||||
|
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
const envSchema = z
|
const envSchema = z
|
||||||
@@ -22,13 +25,17 @@ const envSchema = z
|
|||||||
HSM_LIB_PATH: zpStr(z.string().optional()),
|
HSM_LIB_PATH: zpStr(z.string().optional()),
|
||||||
HSM_PIN: zpStr(z.string().optional()),
|
HSM_PIN: zpStr(z.string().optional()),
|
||||||
HSM_KEY_LABEL: zpStr(z.string().optional()),
|
HSM_KEY_LABEL: zpStr(z.string().optional()),
|
||||||
HSM_SLOT: z.coerce.number().optional().default(0)
|
HSM_SLOT: z.coerce.number().optional().default(0),
|
||||||
|
|
||||||
|
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
|
||||||
|
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
|
||||||
|
LICENSE_KEY: zpStr(z.string().optional()),
|
||||||
|
LICENSE_KEY_OFFLINE: zpStr(z.string().optional()),
|
||||||
|
INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional()),
|
||||||
|
|
||||||
|
SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional()
|
||||||
})
|
})
|
||||||
// To ensure that basic encryption is always possible.
|
// To ensure that basic encryption is always possible.
|
||||||
.refine(
|
|
||||||
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
|
|
||||||
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
|
|
||||||
)
|
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
isHsmConfigured:
|
isHsmConfigured:
|
||||||
@@ -37,7 +44,27 @@ const envSchema = z
|
|||||||
|
|
||||||
export type TMigrationEnvConfig = z.infer<typeof envSchema>;
|
export type TMigrationEnvConfig = z.infer<typeof envSchema>;
|
||||||
|
|
||||||
export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory) => {
|
export const getMigrationHsmConfig = () => {
|
||||||
|
const parsedEnv = envSchema.safeParse(process.env);
|
||||||
|
if (!parsedEnv.success) {
|
||||||
|
console.error("Invalid environment variables. Check the error below");
|
||||||
|
console.error(parsedEnv.error.issues);
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isHsmConfigured: parsedEnv.data.isHsmConfigured,
|
||||||
|
HSM_PIN: parsedEnv.data.HSM_PIN,
|
||||||
|
HSM_SLOT: parsedEnv.data.HSM_SLOT,
|
||||||
|
HSM_LIB_PATH: parsedEnv.data.HSM_LIB_PATH,
|
||||||
|
HSM_KEY_LABEL: parsedEnv.data.HSM_KEY_LABEL
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMigrationEnvConfig = async (
|
||||||
|
superAdminDAL: TSuperAdminDALFactory,
|
||||||
|
hsmService: THsmServiceFactory,
|
||||||
|
kmsRootConfigDAL: TKmsRootConfigDALFactory
|
||||||
|
) => {
|
||||||
const parsedEnv = envSchema.safeParse(process.env);
|
const parsedEnv = envSchema.safeParse(process.env);
|
||||||
if (!parsedEnv.success) {
|
if (!parsedEnv.success) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@@ -53,7 +80,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory
|
|||||||
|
|
||||||
let envCfg = Object.freeze(parsedEnv.data);
|
let envCfg = Object.freeze(parsedEnv.data);
|
||||||
|
|
||||||
const fipsEnabled = await crypto.initialize(superAdminDAL, envCfg);
|
const fipsEnabled = await crypto.initialize(superAdminDAL, hsmService, kmsRootConfigDAL, envCfg);
|
||||||
|
|
||||||
// Fix for 128-bit entropy encryption key expansion issue:
|
// Fix for 128-bit entropy encryption key expansion issue:
|
||||||
// In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY.
|
// In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY.
|
||||||
|
|||||||
@@ -1,28 +1,23 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
import { initializeHsmModule, isHsmActiveAndEnabled } from "@app/ee/services/hsm/hsm-fns";
|
||||||
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
|
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
|
||||||
|
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||||
|
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
|
||||||
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
|
|
||||||
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
|
||||||
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
|
||||||
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
|
||||||
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
|
||||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
import { roleDALFactory } from "@app/services/role/role-dal";
|
||||||
import { secretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
||||||
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
|
||||||
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
|
||||||
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
|
||||||
import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
|
||||||
import { userDALFactory } from "@app/services/user/user-dal";
|
import { userDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
import { TMigrationEnvConfig } from "./env-config";
|
import { TMigrationEnvConfig } from "./env-config";
|
||||||
@@ -33,8 +28,11 @@ type TDependencies = {
|
|||||||
keyStore: TKeyStoreFactory;
|
keyStore: TKeyStoreFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => {
|
type THsmServiceDependencies = {
|
||||||
// eslint-disable-next-line no-param-reassign
|
envConfig: Pick<TMigrationEnvConfig, "HSM_PIN" | "HSM_SLOT" | "HSM_LIB_PATH" | "HSM_KEY_LABEL" | "isHsmConfigured">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMigrationHsmService = async ({ envConfig }: THsmServiceDependencies) => {
|
||||||
const hsmModule = initializeHsmModule(envConfig);
|
const hsmModule = initializeHsmModule(envConfig);
|
||||||
hsmModule.initialize();
|
hsmModule.initialize();
|
||||||
|
|
||||||
@@ -43,67 +41,72 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
|
|||||||
envConfig
|
envConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
const orgDAL = orgDALFactory(db);
|
|
||||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
|
||||||
const projectDAL = projectDALFactory(db);
|
|
||||||
|
|
||||||
const kmsService = kmsServiceFactory({
|
|
||||||
kmsRootConfigDAL,
|
|
||||||
keyStore,
|
|
||||||
kmsDAL,
|
|
||||||
internalKmsDAL,
|
|
||||||
orgDAL,
|
|
||||||
projectDAL,
|
|
||||||
hsmService,
|
|
||||||
envConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
await hsmService.startService();
|
await hsmService.startService();
|
||||||
await kmsService.startService();
|
|
||||||
|
|
||||||
return { kmsService };
|
return { hsmService };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMigrationPITServices = async ({
|
export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => {
|
||||||
db,
|
// ----- DAL dependencies -----
|
||||||
keyStore,
|
const orgDAL = orgDALFactory(db);
|
||||||
envConfig
|
const licenseDAL = licenseDALFactory(db);
|
||||||
}: {
|
const permissionDAL = permissionDALFactory(db);
|
||||||
db: Knex;
|
|
||||||
keyStore: TKeyStoreFactory;
|
|
||||||
envConfig: TMigrationEnvConfig;
|
|
||||||
}) => {
|
|
||||||
const projectDAL = projectDALFactory(db);
|
const projectDAL = projectDALFactory(db);
|
||||||
const folderCommitDAL = folderCommitDALFactory(db);
|
const roleDAL = roleDALFactory(db);
|
||||||
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
|
|
||||||
const folderCheckpointDAL = folderCheckpointDALFactory(db);
|
|
||||||
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
|
|
||||||
const userDAL = userDALFactory(db);
|
const userDAL = userDALFactory(db);
|
||||||
const identityDAL = identityDALFactory(db);
|
const identityDAL = identityDALFactory(db);
|
||||||
const folderDAL = secretFolderDALFactory(db);
|
const serviceTokenDAL = serviceTokenDALFactory(db);
|
||||||
const folderVersionDAL = secretFolderVersionDALFactory(db);
|
|
||||||
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
|
||||||
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
|
||||||
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
|
||||||
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
|
||||||
const secretTagDAL = secretTagDALFactory(db);
|
|
||||||
|
|
||||||
const orgDAL = orgDALFactory(db);
|
|
||||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
const resourceMetadataDAL = resourceMetadataDALFactory(db);
|
|
||||||
|
|
||||||
const hsmModule = initializeHsmModule(envConfig);
|
// ----- Service dependencies -----
|
||||||
hsmModule.initialize();
|
const permissionService = permissionServiceFactory({
|
||||||
|
permissionDAL,
|
||||||
|
serviceTokenDAL,
|
||||||
|
projectDAL,
|
||||||
|
keyStore,
|
||||||
|
roleDAL,
|
||||||
|
userDAL,
|
||||||
|
identityDAL
|
||||||
|
});
|
||||||
|
|
||||||
const hsmService = hsmServiceFactory({
|
const licenseService = licenseServiceFactory({
|
||||||
hsmModule: hsmModule.getModule(),
|
permissionService,
|
||||||
|
orgDAL,
|
||||||
|
licenseDAL,
|
||||||
|
keyStore,
|
||||||
|
projectDAL,
|
||||||
envConfig
|
envConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- HSM startup -----
|
||||||
|
|
||||||
|
const { hsmService } = await getMigrationHsmService({ envConfig });
|
||||||
|
|
||||||
|
const hsmStatus = await isHsmActiveAndEnabled({
|
||||||
|
hsmService,
|
||||||
|
kmsRootConfigDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the encryption strategy is software - user needs to provide an encryption key
|
||||||
|
// if the encryption strategy is null AND the hsm is not configured - user needs to provide an encryption key
|
||||||
|
const needsEncryptionKey =
|
||||||
|
hsmStatus.rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.Software ||
|
||||||
|
(hsmStatus.rootKmsConfigEncryptionStrategy === null && !hsmStatus.isHsmConfigured);
|
||||||
|
|
||||||
|
if (needsEncryptionKey) {
|
||||||
|
if (!envConfig.ROOT_ENCRYPTION_KEY && !envConfig.ENCRYPTION_KEY) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Root KMS encryption strategy is set to software. Please set the ENCRYPTION_KEY environment variable and restart your deployment.\nYou can enable HSM encryption in the Server Console."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- KMS startup -----
|
||||||
|
|
||||||
const kmsService = kmsServiceFactory({
|
const kmsService = kmsServiceFactory({
|
||||||
kmsRootConfigDAL,
|
kmsRootConfigDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -115,27 +118,7 @@ export const getMigrationPITServices = async ({
|
|||||||
envConfig
|
envConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
await hsmService.startService();
|
await kmsService.startService(hsmStatus);
|
||||||
await kmsService.startService();
|
|
||||||
|
|
||||||
const folderCommitService = folderCommitServiceFactory({
|
return { kmsService, hsmService };
|
||||||
folderCommitDAL,
|
|
||||||
folderCommitChangesDAL,
|
|
||||||
folderCheckpointDAL,
|
|
||||||
folderTreeCheckpointDAL,
|
|
||||||
userDAL,
|
|
||||||
identityDAL,
|
|
||||||
folderDAL,
|
|
||||||
folderVersionDAL,
|
|
||||||
secretVersionV2BridgeDAL,
|
|
||||||
projectDAL,
|
|
||||||
folderCheckpointResourcesDAL,
|
|
||||||
secretV2BridgeDAL,
|
|
||||||
folderTreeCheckpointResourcesDAL,
|
|
||||||
kmsService,
|
|
||||||
secretTagDAL,
|
|
||||||
resourceMetadataDAL
|
|
||||||
});
|
|
||||||
|
|
||||||
return { folderCommitService };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ export const CertificatesSchema = z.object({
|
|||||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
pkiSubscriberId: z.string().uuid().nullable().optional(),
|
pkiSubscriberId: z.string().uuid().nullable().optional(),
|
||||||
profileId: z.string().uuid().nullable().optional()
|
profileId: z.string().uuid().nullable().optional(),
|
||||||
|
renewBeforeDays: z.number().nullable().optional(),
|
||||||
|
renewedFromCertificateId: z.string().uuid().nullable().optional(),
|
||||||
|
renewedByCertificateId: z.string().uuid().nullable().optional(),
|
||||||
|
renewalError: z.string().nullable().optional(),
|
||||||
|
keyAlgorithm: z.string().nullable().optional(),
|
||||||
|
signatureAlgorithm: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export const PamAccountsSchema = z.object({
|
|||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
encryptedCredentials: zodBuffer,
|
encryptedCredentials: zodBuffer,
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
rotationEnabled: z.boolean().default(false),
|
||||||
|
rotationIntervalSeconds: z.number().nullable().optional(),
|
||||||
|
lastRotatedAt: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TPamAccounts = z.infer<typeof PamAccountsSchema>;
|
export type TPamAccounts = z.infer<typeof PamAccountsSchema>;
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export const PamResourcesSchema = z.object({
|
|||||||
resourceType: z.string(),
|
resourceType: z.string(),
|
||||||
encryptedConnectionDetails: zodBuffer,
|
encryptedConnectionDetails: zodBuffer,
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
encryptedRotationAccountCredentials: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TPamResources = z.infer<typeof PamResourcesSchema>;
|
export type TPamResources = z.infer<typeof PamResourcesSchema>;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const PkiApiEnrollmentConfigsSchema = z.object({
|
export const PkiApiEnrollmentConfigsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
autoRenew: z.boolean().default(false).nullable().optional(),
|
autoRenew: z.boolean().default(false).nullable().optional(),
|
||||||
autoRenewDays: z.number().nullable().optional(),
|
renewBeforeDays: z.number().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { initEnvConfig } from "@app/lib/config/env";
|
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||||
|
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
|
import { getHsmConfig, initEnvConfig } from "@app/lib/config/env";
|
||||||
import { initLogger, logger } from "@app/lib/logger";
|
import { initLogger, logger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { AuthMethod } from "../../services/auth/auth-type";
|
import { AuthMethod } from "../../services/auth/auth-type";
|
||||||
@@ -17,7 +20,21 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
initLogger();
|
initLogger();
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
await initEnvConfig(superAdminDAL, logger);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
|
||||||
|
const hsmConfig = getHsmConfig(logger);
|
||||||
|
|
||||||
|
const hsmModule = initializeHsmModule(hsmConfig);
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
hsmModule: hsmModule.getModule(),
|
||||||
|
envConfig: hsmConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
await hsmService.startService();
|
||||||
|
|
||||||
|
await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger);
|
||||||
|
|
||||||
await knex(TableName.SuperAdmin).insert([
|
await knex(TableName.SuperAdmin).insert([
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { initEnvConfig } from "@app/lib/config/env";
|
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||||
|
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
|
import { getHsmConfig, initEnvConfig } from "@app/lib/config/env";
|
||||||
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
||||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
import { initLogger, logger } from "@app/lib/logger";
|
import { initLogger, logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
|
import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
|
||||||
import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal";
|
import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal";
|
||||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
|
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
|
||||||
@@ -192,7 +195,21 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
initLogger();
|
initLogger();
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
await initEnvConfig(superAdminDAL, logger);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
|
||||||
|
const hsmConfig = getHsmConfig(logger);
|
||||||
|
|
||||||
|
const hsmModule = initializeHsmModule(hsmConfig);
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
hsmModule: hsmModule.getModule(),
|
||||||
|
envConfig: hsmConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
await hsmService.startService();
|
||||||
|
|
||||||
|
await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger);
|
||||||
|
|
||||||
const [project] = await knex(TableName.Project)
|
const [project] = await knex(TableName.Project)
|
||||||
.insert({
|
.insert({
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { initEnvConfig } from "@app/lib/config/env";
|
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||||
|
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
|
import { getHsmConfig, initEnvConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { initLogger, logger } from "@app/lib/logger";
|
import { initLogger, logger } from "@app/lib/logger";
|
||||||
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
|
import { AccessScope, IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
|
||||||
@@ -15,7 +18,20 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
initLogger();
|
initLogger();
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(knex);
|
const superAdminDAL = superAdminDALFactory(knex);
|
||||||
await initEnvConfig(superAdminDAL, logger);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(knex);
|
||||||
|
const hsmConfig = getHsmConfig(logger);
|
||||||
|
|
||||||
|
const hsmModule = initializeHsmModule(hsmConfig);
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
hsmModule: hsmModule.getModule(),
|
||||||
|
envConfig: hsmConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
await hsmService.startService();
|
||||||
|
|
||||||
|
await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger);
|
||||||
|
|
||||||
// Inserts seed entries
|
// Inserts seed entries
|
||||||
await knex(TableName.Identity).insert([
|
await knex(TableName.Identity).insert([
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
CreateMySQLAccountSchema,
|
||||||
|
SanitizedMySQLAccountWithResourceSchema,
|
||||||
|
UpdateMySQLAccountSchema
|
||||||
|
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
CreatePostgresAccountSchema,
|
CreatePostgresAccountSchema,
|
||||||
@@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
|||||||
createAccountSchema: CreatePostgresAccountSchema,
|
createAccountSchema: CreatePostgresAccountSchema,
|
||||||
updateAccountSchema: UpdatePostgresAccountSchema
|
updateAccountSchema: UpdatePostgresAccountSchema
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||||
|
registerPamResourceEndpoints({
|
||||||
|
server,
|
||||||
|
resourceType: PamResource.MySQL,
|
||||||
|
accountResponseSchema: SanitizedMySQLAccountWithResourceSchema,
|
||||||
|
createAccountSchema: CreateMySQLAccountSchema,
|
||||||
|
updateAccountSchema: UpdateMySQLAccountSchema
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,11 +22,15 @@ export const registerPamResourceEndpoints = <C extends TPamAccount>({
|
|||||||
folderId?: C["folderId"];
|
folderId?: C["folderId"];
|
||||||
name: C["name"];
|
name: C["name"];
|
||||||
description?: C["description"];
|
description?: C["description"];
|
||||||
|
rotationEnabled: C["rotationEnabled"];
|
||||||
|
rotationIntervalSeconds?: C["rotationIntervalSeconds"];
|
||||||
}>;
|
}>;
|
||||||
updateAccountSchema: z.ZodType<{
|
updateAccountSchema: z.ZodType<{
|
||||||
credentials?: C["credentials"];
|
credentials?: C["credentials"];
|
||||||
name?: C["name"];
|
name?: C["name"];
|
||||||
description?: C["description"];
|
description?: C["description"];
|
||||||
|
rotationEnabled?: C["rotationEnabled"];
|
||||||
|
rotationIntervalSeconds?: C["rotationIntervalSeconds"];
|
||||||
}>;
|
}>;
|
||||||
accountResponseSchema: z.ZodTypeAny;
|
accountResponseSchema: z.ZodTypeAny;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -60,7 +64,9 @@ export const registerPamResourceEndpoints = <C extends TPamAccount>({
|
|||||||
resourceType,
|
resourceType,
|
||||||
folderId: req.body.folderId,
|
folderId: req.body.folderId,
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
description: req.body.description
|
description: req.body.description,
|
||||||
|
rotationEnabled: req.body.rotationEnabled,
|
||||||
|
rotationIntervalSeconds: req.body.rotationIntervalSeconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -108,7 +114,9 @@ export const registerPamResourceEndpoints = <C extends TPamAccount>({
|
|||||||
resourceId: account.resourceId,
|
resourceId: account.resourceId,
|
||||||
resourceType,
|
resourceType,
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
description: req.body.description
|
description: req.body.description,
|
||||||
|
rotationEnabled: req.body.rotationEnabled,
|
||||||
|
rotationIntervalSeconds: req.body.rotationIntervalSeconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { PamFoldersSchema } from "@app/db/schemas";
|
import { PamFoldersSchema } 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 { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -10,8 +11,10 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// Use z.union([...]) when more resources are added
|
const SanitizedAccountSchema = z.union([
|
||||||
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema;
|
SanitizedPostgresAccountWithResourceSchema,
|
||||||
|
SanitizedMySQLAccountWithResourceSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
CreatePostgresResourceSchema,
|
CreatePostgresResourceSchema,
|
||||||
PostgresResourceSchema,
|
SanitizedPostgresResourceSchema,
|
||||||
UpdatePostgresResourceSchema
|
UpdatePostgresResourceSchema
|
||||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
|
import {
|
||||||
|
CreateMySQLResourceSchema,
|
||||||
|
MySQLResourceSchema,
|
||||||
|
UpdateMySQLResourceSchema
|
||||||
|
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
|
|
||||||
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
||||||
|
|
||||||
@@ -12,9 +17,18 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
|||||||
registerPamResourceEndpoints({
|
registerPamResourceEndpoints({
|
||||||
server,
|
server,
|
||||||
resourceType: PamResource.Postgres,
|
resourceType: PamResource.Postgres,
|
||||||
resourceResponseSchema: PostgresResourceSchema,
|
resourceResponseSchema: SanitizedPostgresResourceSchema,
|
||||||
createResourceSchema: CreatePostgresResourceSchema,
|
createResourceSchema: CreatePostgresResourceSchema,
|
||||||
updateResourceSchema: UpdatePostgresResourceSchema
|
updateResourceSchema: UpdatePostgresResourceSchema
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||||
|
registerPamResourceEndpoints({
|
||||||
|
server,
|
||||||
|
resourceType: PamResource.MySQL,
|
||||||
|
resourceResponseSchema: MySQLResourceSchema,
|
||||||
|
createResourceSchema: CreateMySQLResourceSchema,
|
||||||
|
updateResourceSchema: UpdateMySQLResourceSchema
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ export const registerPamResourceEndpoints = <T extends TPamResource>({
|
|||||||
connectionDetails: T["connectionDetails"];
|
connectionDetails: T["connectionDetails"];
|
||||||
gatewayId: T["gatewayId"];
|
gatewayId: T["gatewayId"];
|
||||||
name: T["name"];
|
name: T["name"];
|
||||||
|
rotationAccountCredentials?: T["rotationAccountCredentials"];
|
||||||
}>;
|
}>;
|
||||||
updateResourceSchema: z.ZodType<{
|
updateResourceSchema: z.ZodType<{
|
||||||
connectionDetails?: T["connectionDetails"];
|
connectionDetails?: T["connectionDetails"];
|
||||||
gatewayId?: T["gatewayId"];
|
gatewayId?: T["gatewayId"];
|
||||||
name?: T["name"];
|
name?: T["name"];
|
||||||
|
rotationAccountCredentials?: T["rotationAccountCredentials"];
|
||||||
}>;
|
}>;
|
||||||
resourceResponseSchema: z.ZodTypeAny;
|
resourceResponseSchema: z.ZodTypeAny;
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import {
|
||||||
|
MySQLResourceListItemSchema,
|
||||||
|
SanitizedMySQLResourceSchema
|
||||||
|
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import {
|
import {
|
||||||
PostgresResourceListItemSchema,
|
PostgresResourceListItemSchema,
|
||||||
PostgresResourceSchema
|
SanitizedPostgresResourceSchema
|
||||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } 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";
|
||||||
|
|
||||||
// Use z.union([...]) when more resources are added
|
const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
|
||||||
const ResourceSchema = PostgresResourceSchema;
|
|
||||||
|
|
||||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
|
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||||
|
PostgresResourceListItemSchema,
|
||||||
|
MySQLResourceListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -50,7 +56,7 @@ export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
resources: ResourceSchema.array()
|
resources: SanitizedResourceSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { PamSessionsSchema } from "@app/db/schemas";
|
import { PamSessionsSchema } 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 { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// Use z.union([]) once there's multiple
|
const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
|
||||||
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
|
|
||||||
|
|
||||||
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||||
// Meant to be hit solely by gateway identities
|
// Meant to be hit solely by gateway identities
|
||||||
|
|||||||
@@ -340,6 +340,8 @@ export enum EventType {
|
|||||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||||
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
|
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
|
||||||
|
AUTOMATED_RENEW_CERTIFICATE = "automated-renew-certificate",
|
||||||
|
AUTOMATED_RENEW_CERTIFICATE_FAILED = "automated-renew-certificate-failed",
|
||||||
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||||
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
|
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
|
||||||
CREATE_KMS = "create-kms",
|
CREATE_KMS = "create-kms",
|
||||||
@@ -367,6 +369,9 @@ export enum EventType {
|
|||||||
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
|
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
|
||||||
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
|
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
|
||||||
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
|
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
|
||||||
|
RENEW_CERTIFICATE = "renew-certificate",
|
||||||
|
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
|
||||||
|
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
|
||||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
@@ -527,6 +532,8 @@ export enum EventType {
|
|||||||
PAM_ACCOUNT_CREATE = "pam-account-create",
|
PAM_ACCOUNT_CREATE = "pam-account-create",
|
||||||
PAM_ACCOUNT_UPDATE = "pam-account-update",
|
PAM_ACCOUNT_UPDATE = "pam-account-update",
|
||||||
PAM_ACCOUNT_DELETE = "pam-account-delete",
|
PAM_ACCOUNT_DELETE = "pam-account-delete",
|
||||||
|
PAM_ACCOUNT_CREDENTIAL_ROTATION = "pam-account-credential-rotation",
|
||||||
|
PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED = "pam-account-credential-rotation-failed",
|
||||||
PAM_RESOURCE_LIST = "pam-resource-list",
|
PAM_RESOURCE_LIST = "pam-resource-list",
|
||||||
PAM_RESOURCE_GET = "pam-resource-get",
|
PAM_RESOURCE_GET = "pam-resource-get",
|
||||||
PAM_RESOURCE_CREATE = "pam-resource-create",
|
PAM_RESOURCE_CREATE = "pam-resource-create",
|
||||||
@@ -2456,6 +2463,29 @@ interface AutomatedRenewPkiSubscriberCert {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AutomatedRenewCertificate {
|
||||||
|
type: EventType.AUTOMATED_RENEW_CERTIFICATE;
|
||||||
|
metadata: {
|
||||||
|
certificateId: string;
|
||||||
|
commonName: string;
|
||||||
|
profileId: string;
|
||||||
|
renewBeforeDays: string;
|
||||||
|
profileName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutomatedRenewCertificateFailed {
|
||||||
|
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED;
|
||||||
|
metadata: {
|
||||||
|
certificateId: string;
|
||||||
|
commonName: string;
|
||||||
|
profileId: string;
|
||||||
|
renewBeforeDays: string;
|
||||||
|
profileName: string;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SignPkiSubscriberCert {
|
interface SignPkiSubscriberCert {
|
||||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2718,6 +2748,16 @@ interface OrderCertificateFromProfile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RenewCertificate {
|
||||||
|
type: EventType.RENEW_CERTIFICATE;
|
||||||
|
metadata: {
|
||||||
|
originalCertificateId: string;
|
||||||
|
newCertificateId: string;
|
||||||
|
profileName: string;
|
||||||
|
commonName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AttemptCreateSlackIntegration {
|
interface AttemptCreateSlackIntegration {
|
||||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -3915,6 +3955,8 @@ interface PamAccountCreateEvent {
|
|||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
rotationEnabled: boolean;
|
||||||
|
rotationIntervalSeconds?: number | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3926,6 +3968,8 @@ interface PamAccountUpdateEvent {
|
|||||||
resourceType: string;
|
resourceType: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
rotationEnabled?: boolean;
|
||||||
|
rotationIntervalSeconds?: number | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3939,6 +3983,27 @@ interface PamAccountDeleteEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PamAccountCredentialRotationEvent {
|
||||||
|
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION;
|
||||||
|
metadata: {
|
||||||
|
accountName: string;
|
||||||
|
accountId: string;
|
||||||
|
resourceId: string;
|
||||||
|
resourceType: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PamAccountCredentialRotationFailedEvent {
|
||||||
|
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED;
|
||||||
|
metadata: {
|
||||||
|
accountName: string;
|
||||||
|
accountId: string;
|
||||||
|
resourceId: string;
|
||||||
|
resourceType: string;
|
||||||
|
errorMessage: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface PamResourceListEvent {
|
interface PamResourceListEvent {
|
||||||
type: EventType.PAM_RESOURCE_LIST;
|
type: EventType.PAM_RESOURCE_LIST;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -3982,6 +4047,23 @@ interface PamResourceDeleteEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdateCertificateRenewalConfigEvent {
|
||||||
|
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG;
|
||||||
|
metadata: {
|
||||||
|
certificateId: string;
|
||||||
|
renewBeforeDays: string;
|
||||||
|
commonName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisableCertificateRenewalConfigEvent {
|
||||||
|
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG;
|
||||||
|
metadata: {
|
||||||
|
certificateId: string;
|
||||||
|
commonName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| CreateSubOrganizationEvent
|
| CreateSubOrganizationEvent
|
||||||
| UpdateSubOrganizationEvent
|
| UpdateSubOrganizationEvent
|
||||||
@@ -4189,6 +4271,7 @@ export type Event =
|
|||||||
| IssueCertificateFromProfile
|
| IssueCertificateFromProfile
|
||||||
| SignCertificateFromProfile
|
| SignCertificateFromProfile
|
||||||
| OrderCertificateFromProfile
|
| OrderCertificateFromProfile
|
||||||
|
| RenewCertificate
|
||||||
| GetAzureAdCsTemplatesEvent
|
| GetAzureAdCsTemplatesEvent
|
||||||
| AttemptCreateSlackIntegration
|
| AttemptCreateSlackIntegration
|
||||||
| AttemptReinstallSlackIntegration
|
| AttemptReinstallSlackIntegration
|
||||||
@@ -4340,8 +4423,14 @@ export type Event =
|
|||||||
| PamAccountCreateEvent
|
| PamAccountCreateEvent
|
||||||
| PamAccountUpdateEvent
|
| PamAccountUpdateEvent
|
||||||
| PamAccountDeleteEvent
|
| PamAccountDeleteEvent
|
||||||
|
| PamAccountCredentialRotationEvent
|
||||||
|
| PamAccountCredentialRotationFailedEvent
|
||||||
| PamResourceListEvent
|
| PamResourceListEvent
|
||||||
| PamResourceGetEvent
|
| PamResourceGetEvent
|
||||||
| PamResourceCreateEvent
|
| PamResourceCreateEvent
|
||||||
| PamResourceUpdateEvent
|
| PamResourceUpdateEvent
|
||||||
| PamResourceDeleteEvent;
|
| PamResourceDeleteEvent
|
||||||
|
| UpdateCertificateRenewalConfigEvent
|
||||||
|
| DisableCertificateRenewalConfigEvent
|
||||||
|
| AutomatedRenewCertificate
|
||||||
|
| AutomatedRenewCertificateFailed;
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import * as pkcs11js from "pkcs11js";
|
import * as pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
import { TEnvConfig } from "@app/lib/config/env";
|
import { TEnvConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
|
||||||
|
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { THsmServiceFactory } from "./hsm-service";
|
||||||
import { HsmModule } from "./hsm-types";
|
import { HsmModule } from "./hsm-types";
|
||||||
|
|
||||||
export const initializeHsmModule = (envConfig: Pick<TEnvConfig, "isHsmConfigured" | "HSM_LIB_PATH">) => {
|
export const initializeHsmModule = (envConfig: Pick<TEnvConfig, "isHsmConfigured" | "HSM_LIB_PATH">) => {
|
||||||
@@ -25,10 +31,9 @@ export const initializeHsmModule = (envConfig: Pick<TEnvConfig, "isHsmConfigured
|
|||||||
|
|
||||||
logger.info("PKCS#11 module initialized");
|
logger.info("PKCS#11 module initialized");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Failed to initialize PKCS#11 module");
|
|
||||||
|
|
||||||
if ((error as { message?: string })?.message === "CKR_CRYPTOKI_ALREADY_INITIALIZED") {
|
if ((error as { message?: string })?.message === "CKR_CRYPTOKI_ALREADY_INITIALIZED") {
|
||||||
logger.info("Skipping HSM initialization because it's already initialized.");
|
logger.info("Skipping HSM initialization because it's already initialized.");
|
||||||
|
isInitialized = true;
|
||||||
} else {
|
} else {
|
||||||
logger.error(error, "Failed to initialize PKCS#11 module");
|
logger.error(error, "Failed to initialize PKCS#11 module");
|
||||||
throw error;
|
throw error;
|
||||||
@@ -60,3 +65,36 @@ export const initializeHsmModule = (envConfig: Pick<TEnvConfig, "isHsmConfigured
|
|||||||
getModule
|
getModule
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isHsmActiveAndEnabled = async ({
|
||||||
|
hsmService,
|
||||||
|
kmsRootConfigDAL,
|
||||||
|
licenseService
|
||||||
|
}: {
|
||||||
|
hsmService: Pick<THsmServiceFactory, "isActive">;
|
||||||
|
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById">;
|
||||||
|
licenseService?: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||||
|
}) => {
|
||||||
|
const isHsmConfigured = await hsmService.isActive();
|
||||||
|
|
||||||
|
// null if the root kms config does not exist
|
||||||
|
let rootKmsConfigEncryptionStrategy: RootKeyEncryptionStrategy | null = null;
|
||||||
|
|
||||||
|
const rootKmsConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID).catch(() => null);
|
||||||
|
|
||||||
|
rootKmsConfigEncryptionStrategy = (rootKmsConfig?.encryptionStrategy || null) as RootKeyEncryptionStrategy | null;
|
||||||
|
if (
|
||||||
|
rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.HSM &&
|
||||||
|
licenseService &&
|
||||||
|
!licenseService.onPremFeatures.hsm
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Your license does not include HSM integration. Please upgrade to the Enterprise plan to use HSM."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rootKmsConfigEncryptionStrategy,
|
||||||
|
isHsmConfigured
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
const AES_KEY_SIZE = 256;
|
const AES_KEY_SIZE = 256;
|
||||||
const HMAC_KEY_SIZE = 256;
|
const HMAC_KEY_SIZE = 256;
|
||||||
|
|
||||||
|
let pkcs11TestPassed = false;
|
||||||
|
|
||||||
const $withSession = async <T>(callbackWithSession: SessionCallback<T>): Promise<T> => {
|
const $withSession = async <T>(callbackWithSession: SessionCallback<T>): Promise<T> => {
|
||||||
const RETRY_INTERVAL = 200; // 200ms between attempts
|
const RETRY_INTERVAL = 200; // 200ms between attempts
|
||||||
const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time
|
const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time
|
||||||
@@ -363,7 +365,9 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pkcs11TestPassed = false;
|
if (pkcs11TestPassed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pkcs11TestPassed = await $withSession($testPkcs11Module);
|
pkcs11TestPassed = await $withSession($testPkcs11Module);
|
||||||
@@ -371,7 +375,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
logger.error(err, "HSM: Error testing PKCS#11 module");
|
logger.error(err, "HSM: Error testing PKCS#11 module");
|
||||||
}
|
}
|
||||||
|
|
||||||
return envConfig.isHsmConfigured && isInitialized && pkcs11TestPassed;
|
return pkcs11TestPassed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
@@ -460,10 +464,23 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const randomBytes = async (length: number) => {
|
||||||
|
if (!pkcs11 || !isInitialized) {
|
||||||
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomData = await $withSession((sessionHandle) =>
|
||||||
|
pkcs11.C_GenerateRandom(sessionHandle, Buffer.alloc(length))
|
||||||
|
);
|
||||||
|
|
||||||
|
return randomData;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encrypt,
|
encrypt,
|
||||||
startService,
|
startService,
|
||||||
isActive,
|
isActive,
|
||||||
decrypt
|
decrypt,
|
||||||
|
randomBytes
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import pkcs11js from "pkcs11js";
|
import pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
export type HsmModule = {
|
export type HsmModule = {
|
||||||
pkcs11: pkcs11js.PKCS11;
|
pkcs11: pkcs11js.PKCS11;
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
@@ -9,3 +11,8 @@ export enum HsmKeyType {
|
|||||||
AES = "AES",
|
AES = "AES",
|
||||||
HMAC = "hmac"
|
HMAC = "hmac"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type THsmStatus = {
|
||||||
|
rootKmsConfigEncryptionStrategy: RootKeyEncryptionStrategy | null;
|
||||||
|
isHsmConfigured: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { OrganizationActionScope } from "@app/db/schemas";
|
import { OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { TEnvConfig } from "@app/lib/config/env";
|
||||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -40,11 +40,16 @@ import {
|
|||||||
TOrgPlanDTO,
|
TOrgPlanDTO,
|
||||||
TOrgPlansTableDTO,
|
TOrgPlansTableDTO,
|
||||||
TOrgPmtMethodsDTO,
|
TOrgPmtMethodsDTO,
|
||||||
|
TPlanBillingInfo,
|
||||||
TStartOrgTrialDTO,
|
TStartOrgTrialDTO,
|
||||||
TUpdateOrgBillingDetailsDTO
|
TUpdateOrgBillingDetailsDTO
|
||||||
} from "./license-types";
|
} from "./license-types";
|
||||||
|
|
||||||
type TLicenseServiceFactoryDep = {
|
type TLicenseServiceFactoryDep = {
|
||||||
|
envConfig: Pick<
|
||||||
|
TEnvConfig,
|
||||||
|
"LICENSE_SERVER_URL" | "LICENSE_SERVER_KEY" | "LICENSE_KEY" | "LICENSE_KEY_OFFLINE" | "INTERNAL_REGION" | "SITE_URL"
|
||||||
|
>;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findRootOrgDetails" | "countAllOrgMembers" | "findById">;
|
orgDAL: Pick<TOrgDALFactory, "findRootOrgDetails" | "countAllOrgMembers" | "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseDAL: TLicenseDALFactory;
|
licenseDAL: TLicenseDALFactory;
|
||||||
@@ -65,26 +70,26 @@ export const licenseServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
licenseDAL,
|
licenseDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
envConfig
|
||||||
}: TLicenseServiceFactoryDep) => {
|
}: TLicenseServiceFactoryDep) => {
|
||||||
let isValidLicense = false;
|
let isValidLicense = false;
|
||||||
let instanceType = InstanceType.OnPrem;
|
let instanceType = InstanceType.OnPrem;
|
||||||
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
|
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
|
||||||
let selfHostedLicense: TOfflineLicense | null = null;
|
let selfHostedLicense: TOfflineLicense | null = null;
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const licenseServerCloudApi = setupLicenseRequestWithStore(
|
const licenseServerCloudApi = setupLicenseRequestWithStore(
|
||||||
appCfg.LICENSE_SERVER_URL || "",
|
envConfig.LICENSE_SERVER_URL || "",
|
||||||
LICENSE_SERVER_CLOUD_LOGIN,
|
LICENSE_SERVER_CLOUD_LOGIN,
|
||||||
appCfg.LICENSE_SERVER_KEY || "",
|
envConfig.LICENSE_SERVER_KEY || "",
|
||||||
appCfg.INTERNAL_REGION
|
envConfig.INTERNAL_REGION
|
||||||
);
|
);
|
||||||
|
|
||||||
const licenseServerOnPremApi = setupLicenseRequestWithStore(
|
const licenseServerOnPremApi = setupLicenseRequestWithStore(
|
||||||
appCfg.LICENSE_SERVER_URL || "",
|
envConfig.LICENSE_SERVER_URL || "",
|
||||||
LICENSE_SERVER_ON_PREM_LOGIN,
|
LICENSE_SERVER_ON_PREM_LOGIN,
|
||||||
appCfg.LICENSE_KEY || "",
|
envConfig.LICENSE_KEY || "",
|
||||||
appCfg.INTERNAL_REGION
|
envConfig.INTERNAL_REGION
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => {
|
const syncLicenseKeyOnPremFeatures = async (shouldThrow: boolean = false) => {
|
||||||
@@ -118,7 +123,7 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
if (appCfg.LICENSE_SERVER_KEY) {
|
if (envConfig.LICENSE_SERVER_KEY) {
|
||||||
const token = await licenseServerCloudApi.refreshLicense();
|
const token = await licenseServerCloudApi.refreshLicense();
|
||||||
if (token) instanceType = InstanceType.Cloud;
|
if (token) instanceType = InstanceType.Cloud;
|
||||||
logger.info(`Instance type: ${InstanceType.Cloud}`);
|
logger.info(`Instance type: ${InstanceType.Cloud}`);
|
||||||
@@ -126,7 +131,7 @@ export const licenseServiceFactory = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appCfg.LICENSE_KEY) {
|
if (envConfig.LICENSE_KEY) {
|
||||||
const token = await licenseServerOnPremApi.refreshLicense();
|
const token = await licenseServerOnPremApi.refreshLicense();
|
||||||
if (token) {
|
if (token) {
|
||||||
await syncLicenseKeyOnPremFeatures(true);
|
await syncLicenseKeyOnPremFeatures(true);
|
||||||
@@ -137,10 +142,10 @@ export const licenseServiceFactory = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appCfg.LICENSE_KEY_OFFLINE) {
|
if (envConfig.LICENSE_KEY_OFFLINE) {
|
||||||
let isValidOfflineLicense = true;
|
let isValidOfflineLicense = true;
|
||||||
const contents: TOfflineLicenseContents = JSON.parse(
|
const contents: TOfflineLicenseContents = JSON.parse(
|
||||||
Buffer.from(appCfg.LICENSE_KEY_OFFLINE, "base64").toString("utf8")
|
Buffer.from(envConfig.LICENSE_KEY_OFFLINE, "base64").toString("utf8")
|
||||||
);
|
);
|
||||||
const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature);
|
const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature);
|
||||||
|
|
||||||
@@ -179,7 +184,7 @@ export const licenseServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initializeBackgroundSync = async () => {
|
const initializeBackgroundSync = async () => {
|
||||||
if (appCfg.LICENSE_KEY) {
|
if (envConfig.LICENSE_KEY) {
|
||||||
logger.info("Setting up background sync process for refresh onPremFeatures");
|
logger.info("Setting up background sync process for refresh onPremFeatures");
|
||||||
const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures);
|
const job = new CronJob("*/10 * * * *", syncLicenseKeyOnPremFeatures);
|
||||||
job.start();
|
job.start();
|
||||||
@@ -212,9 +217,8 @@ export const licenseServiceFactory = ({
|
|||||||
const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId);
|
const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId);
|
||||||
currentPlan.membersUsed = membersUsed;
|
currentPlan.membersUsed = membersUsed;
|
||||||
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId);
|
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId);
|
||||||
currentPlan.identitiesUsed = identityUsed;
|
|
||||||
|
|
||||||
if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) {
|
if (currentPlan?.identitiesUsed && currentPlan.identitiesUsed !== identityUsed) {
|
||||||
try {
|
try {
|
||||||
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
|
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
|
||||||
quantity: membersUsed,
|
quantity: membersUsed,
|
||||||
@@ -227,6 +231,7 @@ export const licenseServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentPlan.identitiesUsed = identityUsed;
|
||||||
|
|
||||||
await keyStore.setItemWithExpiry(
|
await keyStore.setItemWithExpiry(
|
||||||
FEATURE_CACHE_KEY(org.id),
|
FEATURE_CACHE_KEY(org.id),
|
||||||
@@ -440,8 +445,8 @@ export const licenseServiceFactory = ({
|
|||||||
} = await licenseServerCloudApi.request.post(
|
} = await licenseServerCloudApi.request.post(
|
||||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
|
||||||
{
|
{
|
||||||
success_url: `${appCfg.SITE_URL}/organization/billing`,
|
success_url: `${envConfig.SITE_URL}/organization/billing`,
|
||||||
cancel_url: `${appCfg.SITE_URL}/organization/billing`
|
cancel_url: `${envConfig.SITE_URL}/organization/billing`
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -454,13 +459,28 @@ export const licenseServiceFactory = ({
|
|||||||
} = await licenseServerCloudApi.request.post(
|
} = await licenseServerCloudApi.request.post(
|
||||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`,
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`,
|
||||||
{
|
{
|
||||||
return_url: `${appCfg.SITE_URL}/organization/billing`
|
return_url: `${envConfig.SITE_URL}/organization/billing`
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { url };
|
return { url };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUsageMetrics = async (orgId: string) => {
|
||||||
|
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
|
||||||
|
orgDAL.countAllOrgMembers(orgId),
|
||||||
|
licenseDAL.countOfOrgIdentities(orgId),
|
||||||
|
projectDAL.countOfOrgProjects(orgId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
orgMembersUsed,
|
||||||
|
identityUsed,
|
||||||
|
projectCount,
|
||||||
|
totalIdentities: identityUsed + orgMembersUsed
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission({
|
const { permission } = await permissionService.getOrgPermission({
|
||||||
actorId,
|
actorId,
|
||||||
@@ -479,10 +499,16 @@ export const licenseServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (instanceType === InstanceType.Cloud) {
|
if (instanceType === InstanceType.Cloud) {
|
||||||
const { data } = await licenseServerCloudApi.request.get(
|
const { data } = await licenseServerCloudApi.request.get<TPlanBillingInfo>(
|
||||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
||||||
);
|
);
|
||||||
return data;
|
const { identityUsed, orgMembersUsed } = await getUsageMetrics(orgId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
users: orgMembersUsed,
|
||||||
|
identities: identityUsed
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -491,7 +517,9 @@ export const licenseServiceFactory = ({
|
|||||||
interval: "month",
|
interval: "month",
|
||||||
intervalCount: 1,
|
intervalCount: 1,
|
||||||
amount: 0,
|
amount: 0,
|
||||||
quantity: 1
|
quantity: 1,
|
||||||
|
users: 0,
|
||||||
|
identities: 0
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -535,21 +563,6 @@ export const licenseServiceFactory = ({
|
|||||||
throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`);
|
throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsageMetrics = async (orgId: string) => {
|
|
||||||
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
|
|
||||||
orgDAL.countAllOrgMembers(orgId),
|
|
||||||
licenseDAL.countOfOrgIdentities(orgId),
|
|
||||||
projectDAL.countOfOrgProjects(orgId)
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
orgMembersUsed,
|
|
||||||
identityUsed,
|
|
||||||
projectCount,
|
|
||||||
totalIdentities: identityUsed + orgMembersUsed
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns org current plan feature table
|
// returns org current plan feature table
|
||||||
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission({
|
const { permission } = await permissionService.getOrgPermission({
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ export type TOfflineLicense = {
|
|||||||
features: TFeatureSet;
|
features: TFeatureSet;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TPlanBillingInfo = {
|
||||||
|
currentPeriodStart: number;
|
||||||
|
currentPeriodEnd: number;
|
||||||
|
interval: "month" | "year";
|
||||||
|
intervalCount: number;
|
||||||
|
amount: number;
|
||||||
|
quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type TFeatureSet = {
|
export type TFeatureSet = {
|
||||||
_id: null;
|
_id: null;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export const pamAccountDALFactory = (db: TDbClient) => {
|
|||||||
.select(
|
.select(
|
||||||
// resource
|
// resource
|
||||||
db.ref("name").withSchema(TableName.PamResource).as("resourceName"),
|
db.ref("name").withSchema(TableName.PamResource).as("resourceName"),
|
||||||
db.ref("resourceType").withSchema(TableName.PamResource)
|
db.ref("resourceType").withSchema(TableName.PamResource),
|
||||||
|
db.ref("encryptedRotationAccountCredentials").withSchema(TableName.PamResource)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@@ -28,16 +29,35 @@ export const pamAccountDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const accounts = await query;
|
const accounts = await query;
|
||||||
|
|
||||||
return accounts.map(({ resourceId, resourceName, resourceType, ...account }) => ({
|
return accounts.map(
|
||||||
...account,
|
({ resourceId, resourceName, resourceType, encryptedRotationAccountCredentials, ...account }) => ({
|
||||||
resourceId,
|
...account,
|
||||||
resource: {
|
resourceId,
|
||||||
id: resourceId,
|
resource: {
|
||||||
name: resourceName,
|
id: resourceId,
|
||||||
resourceType
|
name: resourceName,
|
||||||
}
|
resourceType,
|
||||||
}));
|
encryptedRotationAccountCredentials
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...orm, findWithResourceDetails };
|
const findAccountsDueForRotation = async (tx?: Knex) => {
|
||||||
|
const dbClient = tx || db.replicaNode();
|
||||||
|
|
||||||
|
const accounts = await dbClient(TableName.PamAccount)
|
||||||
|
.innerJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`)
|
||||||
|
.whereNotNull(`${TableName.PamResource}.encryptedRotationAccountCredentials`)
|
||||||
|
.whereNotNull(`${TableName.PamAccount}.rotationIntervalSeconds`)
|
||||||
|
.where(`${TableName.PamAccount}.rotationEnabled`, true)
|
||||||
|
.whereRaw(
|
||||||
|
`COALESCE("${TableName.PamAccount}"."lastRotatedAt", "${TableName.PamAccount}"."createdAt") + "${TableName.PamAccount}"."rotationIntervalSeconds" * interval '1 second' < NOW()`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.PamAccount));
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...orm, findWithResourceDetails, findAccountsDueForRotation };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types";
|
||||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal";
|
import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal";
|
||||||
@@ -45,10 +47,12 @@ type TPamAccountServiceFactoryDep = {
|
|||||||
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
|
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
|
||||||
>;
|
>;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
|
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
|
||||||
|
|
||||||
|
const ROTATION_CONCURRENCY_LIMIT = 10;
|
||||||
|
|
||||||
export const pamAccountServiceFactory = ({
|
export const pamAccountServiceFactory = ({
|
||||||
pamResourceDAL,
|
pamResourceDAL,
|
||||||
pamSessionDAL,
|
pamSessionDAL,
|
||||||
@@ -59,10 +63,19 @@ export const pamAccountServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
gatewayV2Service
|
gatewayV2Service,
|
||||||
|
auditLogService
|
||||||
}: TPamAccountServiceFactoryDep) => {
|
}: TPamAccountServiceFactoryDep) => {
|
||||||
const create = async (
|
const create = async (
|
||||||
{ credentials, resourceId, name, description, folderId }: TCreateAccountDTO,
|
{
|
||||||
|
credentials,
|
||||||
|
resourceId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
folderId,
|
||||||
|
rotationEnabled,
|
||||||
|
rotationIntervalSeconds
|
||||||
|
}: TCreateAccountDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||||
@@ -72,6 +85,12 @@ export const pamAccountServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rotationEnabled && (rotationIntervalSeconds === undefined || rotationIntervalSeconds === null)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Rotation interval must be defined when rotation is enabled."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const resource = await pamResourceDAL.findById(resourceId);
|
const resource = await pamResourceDAL.findById(resourceId);
|
||||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
|
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
|
||||||
|
|
||||||
@@ -84,6 +103,10 @@ export const pamAccountServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.PAM
|
actionProjectType: ActionProjectType.PAM
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!resource.encryptedRotationAccountCredentials && rotationEnabled) {
|
||||||
|
throw new NotFoundError({ message: "Rotation credentials are not configured for this account's resource" });
|
||||||
|
}
|
||||||
|
|
||||||
const accountPath = await getFullPamFolderPath({
|
const accountPath = await getFullPamFolderPath({
|
||||||
pamFolderDAL,
|
pamFolderDAL,
|
||||||
folderId,
|
folderId,
|
||||||
@@ -126,12 +149,19 @@ export const pamAccountServiceFactory = ({
|
|||||||
encryptedCredentials,
|
encryptedCredentials,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
folderId
|
folderId,
|
||||||
|
rotationEnabled,
|
||||||
|
rotationIntervalSeconds
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(await decryptAccount(account, resource.projectId, kmsService)),
|
...(await decryptAccount(account, resource.projectId, kmsService)),
|
||||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
resource: {
|
||||||
|
id: resource.id,
|
||||||
|
name: resource.name,
|
||||||
|
resourceType: resource.resourceType,
|
||||||
|
rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||||
@@ -145,7 +175,7 @@ export const pamAccountServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateById = async (
|
const updateById = async (
|
||||||
{ accountId, credentials, description, name }: TUpdateAccountDTO,
|
{ accountId, credentials, description, name, rotationEnabled, rotationIntervalSeconds }: TUpdateAccountDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||||
@@ -195,6 +225,17 @@ export const pamAccountServiceFactory = ({
|
|||||||
updateDoc.description = description;
|
updateDoc.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rotationEnabled !== undefined) {
|
||||||
|
if (!resource.encryptedRotationAccountCredentials && rotationEnabled) {
|
||||||
|
throw new NotFoundError({ message: "Rotation credentials are not configured for this account's resource" });
|
||||||
|
}
|
||||||
|
updateDoc.rotationEnabled = rotationEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotationIntervalSeconds !== undefined) {
|
||||||
|
updateDoc.rotationIntervalSeconds = rotationIntervalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
if (credentials !== undefined) {
|
if (credentials !== undefined) {
|
||||||
const connectionDetails = await decryptResourceConnectionDetails({
|
const connectionDetails = await decryptResourceConnectionDetails({
|
||||||
projectId: account.projectId,
|
projectId: account.projectId,
|
||||||
@@ -211,7 +252,7 @@ export const pamAccountServiceFactory = ({
|
|||||||
|
|
||||||
// Logic to prevent overwriting unedited censored values
|
// Logic to prevent overwriting unedited censored values
|
||||||
const finalCredentials = { ...credentials };
|
const finalCredentials = { ...credentials };
|
||||||
if (credentials.password === "******") {
|
if (credentials.password === "__INFISICAL_UNCHANGED__") {
|
||||||
const decryptedCredentials = await decryptAccountCredentials({
|
const decryptedCredentials = await decryptAccountCredentials({
|
||||||
encryptedCredentials: account.encryptedCredentials,
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
projectId: account.projectId,
|
projectId: account.projectId,
|
||||||
@@ -239,7 +280,12 @@ export const pamAccountServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...(await decryptAccount(updatedAccount, account.projectId, kmsService)),
|
...(await decryptAccount(updatedAccount, account.projectId, kmsService)),
|
||||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
resource: {
|
||||||
|
id: resource.id,
|
||||||
|
name: resource.name,
|
||||||
|
resourceType: resource.resourceType,
|
||||||
|
rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,7 +324,12 @@ export const pamAccountServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...(await decryptAccount(deletedAccount, account.projectId, kmsService)),
|
...(await decryptAccount(deletedAccount, account.projectId, kmsService)),
|
||||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
resource: {
|
||||||
|
id: resource.id,
|
||||||
|
name: resource.name,
|
||||||
|
resourceType: resource.resourceType,
|
||||||
|
rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,7 +351,7 @@ export const pamAccountServiceFactory = ({
|
|||||||
|
|
||||||
const decryptedAndPermittedAccounts: Array<
|
const decryptedAndPermittedAccounts: Array<
|
||||||
TPamAccounts & {
|
TPamAccounts & {
|
||||||
resource: Pick<TPamResources, "id" | "name" | "resourceType">;
|
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
|
||||||
credentials: TPamAccountCredentials;
|
credentials: TPamAccountCredentials;
|
||||||
}
|
}
|
||||||
> = [];
|
> = [];
|
||||||
@@ -330,7 +381,8 @@ export const pamAccountServiceFactory = ({
|
|||||||
resource: {
|
resource: {
|
||||||
id: account.resource.id,
|
id: account.resource.id,
|
||||||
name: account.resource.name,
|
name: account.resource.name,
|
||||||
resourceType: account.resource.resourceType
|
resourceType: account.resource.resourceType,
|
||||||
|
rotationCredentialsConfigured: !!account.resource.encryptedRotationAccountCredentials
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -517,12 +569,116 @@ export const pamAccountServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rotateAllDueAccounts = async () => {
|
||||||
|
const accounts = await pamAccountDAL.findAccountsDueForRotation();
|
||||||
|
|
||||||
|
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
|
||||||
|
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
|
||||||
|
|
||||||
|
const rotationPromises = batch.map(async (account) =>
|
||||||
|
pamAccountDAL.transaction(async (tx) => {
|
||||||
|
let logResourceType = "unknown";
|
||||||
|
try {
|
||||||
|
const resource = await pamResourceDAL.findById(account.resourceId, tx);
|
||||||
|
if (!resource || !resource.encryptedRotationAccountCredentials) return;
|
||||||
|
logResourceType = resource.resourceType;
|
||||||
|
|
||||||
|
const { connectionDetails, rotationAccountCredentials, gatewayId, resourceType } = await decryptResource(
|
||||||
|
resource,
|
||||||
|
account.projectId,
|
||||||
|
kmsService
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rotationAccountCredentials) return;
|
||||||
|
|
||||||
|
const accountCredentials = await decryptAccountCredentials({
|
||||||
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
|
projectId: account.projectId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const factory = PAM_RESOURCE_FACTORY_MAP[resourceType as PamResource](
|
||||||
|
resourceType as PamResource,
|
||||||
|
connectionDetails,
|
||||||
|
gatewayId,
|
||||||
|
gatewayV2Service
|
||||||
|
);
|
||||||
|
|
||||||
|
const newCredentials = await factory.rotateAccountCredentials(
|
||||||
|
rotationAccountCredentials,
|
||||||
|
accountCredentials
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAccountCredentials({
|
||||||
|
credentials: newCredentials,
|
||||||
|
projectId: account.projectId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await pamAccountDAL.updateById(
|
||||||
|
account.id,
|
||||||
|
{
|
||||||
|
encryptedCredentials,
|
||||||
|
lastRotatedAt: new Date()
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: account.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION,
|
||||||
|
metadata: {
|
||||||
|
accountId: account.id,
|
||||||
|
accountName: account.name,
|
||||||
|
resourceId: resource.id,
|
||||||
|
resourceType: logResourceType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
||||||
|
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: account.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
|
||||||
|
metadata: {
|
||||||
|
accountId: account.id,
|
||||||
|
accountName: account.name,
|
||||||
|
resourceId: account.resourceId,
|
||||||
|
resourceType: logResourceType,
|
||||||
|
errorMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw error; // Rollback transaction
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await Promise.all(rotationPromises);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
deleteById,
|
deleteById,
|
||||||
list,
|
list,
|
||||||
access,
|
access,
|
||||||
getSessionCredentials
|
getSessionCredentials,
|
||||||
|
rotateAllDueAccounts
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { TPamAccount } from "../pam-resource/pam-resource-types";
|
import { TPamAccount } from "../pam-resource/pam-resource-types";
|
||||||
|
|
||||||
// DTOs
|
// DTOs
|
||||||
export type TCreateAccountDTO = Pick<TPamAccount, "name" | "description" | "credentials" | "folderId" | "resourceId">;
|
export type TCreateAccountDTO = Pick<
|
||||||
|
TPamAccount,
|
||||||
|
"name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationEnabled" | "rotationIntervalSeconds"
|
||||||
|
>;
|
||||||
|
|
||||||
export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "resourceId">> & {
|
export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "resourceId">> & {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { MySQLResourceListItemSchema } from "./mysql-resource-schemas";
|
||||||
|
|
||||||
|
export const getMySQLResourceListItem = () => {
|
||||||
|
return {
|
||||||
|
name: MySQLResourceListItemSchema.shape.name.value,
|
||||||
|
resource: MySQLResourceListItemSchema.shape.resource.value
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { PamResource } from "../pam-resource-enums";
|
||||||
|
import {
|
||||||
|
BaseCreatePamAccountSchema,
|
||||||
|
BaseCreatePamResourceSchema,
|
||||||
|
BasePamAccountSchema,
|
||||||
|
BasePamAccountSchemaWithResource,
|
||||||
|
BasePamResourceSchema,
|
||||||
|
BaseUpdatePamAccountSchema,
|
||||||
|
BaseUpdatePamResourceSchema
|
||||||
|
} from "../pam-resource-schemas";
|
||||||
|
import {
|
||||||
|
BaseSqlAccountCredentialsSchema,
|
||||||
|
BaseSqlResourceConnectionDetailsSchema
|
||||||
|
} from "../shared/sql/sql-resource-schemas";
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
export const MySQLResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema.extend({
|
||||||
|
// MySQL db in many cases the db will not be provided when making connection
|
||||||
|
database: z.string().trim()
|
||||||
|
});
|
||||||
|
export const MySQLAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
|
||||||
|
|
||||||
|
const BaseMySQLResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.MySQL) });
|
||||||
|
|
||||||
|
export const MySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedMySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.pick({
|
||||||
|
username: true
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MySQLResourceListItemSchema = z.object({
|
||||||
|
name: z.literal("MySQL"),
|
||||||
|
resource: z.literal(PamResource.MySQL)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema.optional(),
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export const MySQLAccountSchema = BasePamAccountSchema.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateMySQLAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateMySQLAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedMySQLAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema.pick({
|
||||||
|
username: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
export const MySQLSessionCredentialsSchema = MySQLResourceConnectionDetailsSchema.and(MySQLAccountCredentialsSchema);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MySQLAccountCredentialsSchema,
|
||||||
|
MySQLAccountSchema,
|
||||||
|
MySQLResourceConnectionDetailsSchema,
|
||||||
|
MySQLResourceSchema
|
||||||
|
} from "./mysql-resource-schemas";
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
export type TMySQLResource = z.infer<typeof MySQLResourceSchema>;
|
||||||
|
export type TMySQLResourceConnectionDetails = z.infer<typeof MySQLResourceConnectionDetailsSchema>;
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export type TMySQLAccount = z.infer<typeof MySQLAccountSchema>;
|
||||||
|
export type TMySQLAccountCredentials = z.infer<typeof MySQLAccountCredentialsSchema>;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export enum PamResource {
|
export enum PamResource {
|
||||||
Postgres = "postgres"
|
Postgres = "postgres",
|
||||||
|
MySQL = "mysql"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
|||||||
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
||||||
|
|
||||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation
|
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||||
|
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { TPamResources } from "@app/db/schemas";
|
|||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||||
|
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
|
||||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||||
|
|
||||||
export const listResourceOptions = () => {
|
export const listResourceOptions = () => {
|
||||||
return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resource
|
// Resource
|
||||||
@@ -63,6 +65,13 @@ export const decryptResource = async (
|
|||||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||||
projectId,
|
projectId,
|
||||||
kmsService
|
kmsService
|
||||||
})
|
}),
|
||||||
|
rotationAccountCredentials: resource.encryptedRotationAccountCredentials
|
||||||
|
? await decryptAccountCredentials({
|
||||||
|
encryptedCredentials: resource.encryptedRotationAccountCredentials,
|
||||||
|
projectId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
: null
|
||||||
} as TPamResource;
|
} as TPamResource;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { slugSchema } from "@app/server/lib/schemas";
|
|||||||
// Resources
|
// Resources
|
||||||
export const BasePamResourceSchema = PamResourcesSchema.omit({
|
export const BasePamResourceSchema = PamResourcesSchema.omit({
|
||||||
encryptedConnectionDetails: true,
|
encryptedConnectionDetails: true,
|
||||||
|
encryptedRotationAccountCredentials: true,
|
||||||
resourceType: true
|
resourceType: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
resourceType: true
|
resourceType: true
|
||||||
|
}).extend({
|
||||||
|
rotationCredentialsConfigured: z.boolean()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,10 +40,14 @@ export const BaseCreatePamAccountSchema = z.object({
|
|||||||
resourceId: z.string().uuid(),
|
resourceId: z.string().uuid(),
|
||||||
folderId: z.string().uuid().optional(),
|
folderId: z.string().uuid().optional(),
|
||||||
name: slugSchema({ field: "name" }),
|
name: slugSchema({ field: "name" }),
|
||||||
description: z.string().max(512).nullable().optional()
|
description: z.string().max(512).nullable().optional(),
|
||||||
|
rotationEnabled: z.boolean(),
|
||||||
|
rotationIntervalSeconds: z.number().min(3600).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BaseUpdatePamAccountSchema = z.object({
|
export const BaseUpdatePamAccountSchema = z.object({
|
||||||
name: slugSchema({ field: "name" }).optional(),
|
name: slugSchema({ field: "name" }).optional(),
|
||||||
description: z.string().max(512).nullable().optional()
|
description: z.string().max(512).nullable().optional(),
|
||||||
|
rotationEnabled: z.boolean().optional(),
|
||||||
|
rotationIntervalSeconds: z.number().min(3600).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
|
|
||||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { decryptAccountCredentials, encryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||||
import { TPamResourceDALFactory } from "./pam-resource-dal";
|
import { TPamResourceDALFactory } from "./pam-resource-dal";
|
||||||
import { PamResource } from "./pam-resource-enums";
|
import { PamResource } from "./pam-resource-enums";
|
||||||
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
|
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
|
||||||
import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns";
|
import {
|
||||||
|
decryptResource,
|
||||||
|
decryptResourceConnectionDetails,
|
||||||
|
encryptResourceConnectionDetails,
|
||||||
|
listResourceOptions
|
||||||
|
} from "./pam-resource-fns";
|
||||||
import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types";
|
import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types";
|
||||||
|
|
||||||
type TPamResourceServiceFactoryDep = {
|
type TPamResourceServiceFactoryDep = {
|
||||||
@@ -61,7 +67,7 @@ export const pamResourceServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const create = async (
|
const create = async (
|
||||||
{ resourceType, connectionDetails, gatewayId, name, projectId }: TCreateResourceDTO,
|
{ resourceType, connectionDetails, gatewayId, name, projectId, rotationAccountCredentials }: TCreateResourceDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||||
@@ -88,26 +94,42 @@ export const pamResourceServiceFactory = ({
|
|||||||
gatewayId,
|
gatewayId,
|
||||||
gatewayV2Service
|
gatewayV2Service
|
||||||
);
|
);
|
||||||
const validatedConnectionDetails = await factory.validateConnection();
|
|
||||||
|
|
||||||
|
const validatedConnectionDetails = await factory.validateConnection();
|
||||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||||
connectionDetails: validatedConnectionDetails,
|
connectionDetails: validatedConnectionDetails,
|
||||||
projectId,
|
projectId,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let encryptedRotationAccountCredentials: Buffer | null = null;
|
||||||
|
|
||||||
|
if (rotationAccountCredentials) {
|
||||||
|
const validatedRotationAccountCredentials = await factory.validateAccountCredentials(rotationAccountCredentials);
|
||||||
|
|
||||||
|
encryptedRotationAccountCredentials = await encryptAccountCredentials({
|
||||||
|
credentials: validatedRotationAccountCredentials,
|
||||||
|
projectId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const resource = await pamResourceDAL.create({
|
const resource = await pamResourceDAL.create({
|
||||||
resourceType,
|
resourceType,
|
||||||
encryptedConnectionDetails,
|
encryptedConnectionDetails,
|
||||||
gatewayId,
|
gatewayId,
|
||||||
name,
|
name,
|
||||||
projectId
|
projectId,
|
||||||
|
encryptedRotationAccountCredentials
|
||||||
});
|
});
|
||||||
|
|
||||||
return decryptResource(resource, projectId, kmsService);
|
return decryptResource(resource, projectId, kmsService);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => {
|
const updateById = async (
|
||||||
|
{ connectionDetails, resourceId, name, rotationAccountCredentials }: TUpdateResourceDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||||
if (!orgLicensePlan.pam) {
|
if (!orgLicensePlan.pam) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -151,6 +173,60 @@ export const pamResourceServiceFactory = ({
|
|||||||
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
|
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rotationAccountCredentials !== undefined) {
|
||||||
|
updateDoc.encryptedRotationAccountCredentials = null;
|
||||||
|
|
||||||
|
if (rotationAccountCredentials) {
|
||||||
|
const decryptedConnectionDetails =
|
||||||
|
connectionDetails ??
|
||||||
|
(await decryptResourceConnectionDetails({
|
||||||
|
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||||
|
projectId: resource.projectId,
|
||||||
|
kmsService
|
||||||
|
}));
|
||||||
|
|
||||||
|
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
|
||||||
|
resource.resourceType as PamResource,
|
||||||
|
decryptedConnectionDetails,
|
||||||
|
resource.gatewayId,
|
||||||
|
gatewayV2Service
|
||||||
|
);
|
||||||
|
|
||||||
|
// Logic to prevent overwriting unedited censored values
|
||||||
|
const finalCredentials = { ...rotationAccountCredentials };
|
||||||
|
if (
|
||||||
|
resource.encryptedRotationAccountCredentials &&
|
||||||
|
rotationAccountCredentials.password === "__INFISICAL_UNCHANGED__"
|
||||||
|
) {
|
||||||
|
const decryptedCredentials = await decryptAccountCredentials({
|
||||||
|
encryptedCredentials: resource.encryptedRotationAccountCredentials,
|
||||||
|
projectId: resource.projectId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
finalCredentials.password = decryptedCredentials.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validatedRotationAccountCredentials = await factory.validateAccountCredentials(finalCredentials);
|
||||||
|
|
||||||
|
updateDoc.encryptedRotationAccountCredentials = await encryptAccountCredentials({
|
||||||
|
credentials: validatedRotationAccountCredentials,
|
||||||
|
projectId: resource.projectId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof BadRequestError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Rotation Account Error: ${err.message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If nothing was updated, return the fetched resource
|
// If nothing was updated, return the fetched resource
|
||||||
if (Object.keys(updateDoc).length === 0) {
|
if (Object.keys(updateDoc).length === 0) {
|
||||||
return decryptResource(resource, resource.projectId, kmsService);
|
return decryptResource(resource, resource.projectId, kmsService);
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||||
|
import {
|
||||||
|
TMySQLAccount,
|
||||||
|
TMySQLAccountCredentials,
|
||||||
|
TMySQLResource,
|
||||||
|
TMySQLResourceConnectionDetails
|
||||||
|
} from "./mysql/mysql-resource-types";
|
||||||
import { PamResource } from "./pam-resource-enums";
|
import { PamResource } from "./pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
TPostgresAccount,
|
TPostgresAccount,
|
||||||
@@ -8,17 +14,18 @@ import {
|
|||||||
} from "./postgres/postgres-resource-types";
|
} from "./postgres/postgres-resource-types";
|
||||||
|
|
||||||
// Resource types
|
// Resource types
|
||||||
export type TPamResource = TPostgresResource;
|
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||||
|
|
||||||
// Account types
|
// Account types
|
||||||
export type TPamAccount = TPostgresAccount;
|
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
||||||
export type TPamAccountCredentials = TPostgresAccountCredentials;
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
|
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||||
|
|
||||||
// Resource DTOs
|
// Resource DTOs
|
||||||
export type TCreateResourceDTO = Pick<
|
export type TCreateResourceDTO = Pick<
|
||||||
TPamResource,
|
TPamResource,
|
||||||
"name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId"
|
"name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" | "rotationAccountCredentials"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type TUpdateResourceDTO = Partial<Omit<TCreateResourceDTO, "resourceType" | "projectId">> & {
|
export type TUpdateResourceDTO = Partial<Omit<TCreateResourceDTO, "resourceType" | "projectId">> & {
|
||||||
@@ -30,6 +37,10 @@ export type TPamResourceFactoryValidateConnection<T extends TPamResourceConnecti
|
|||||||
export type TPamResourceFactoryValidateAccountCredentials<C extends TPamAccountCredentials> = (
|
export type TPamResourceFactoryValidateAccountCredentials<C extends TPamAccountCredentials> = (
|
||||||
credentials: C
|
credentials: C
|
||||||
) => Promise<C>;
|
) => Promise<C>;
|
||||||
|
export type TPamResourceFactoryRotateAccountCredentials<C extends TPamAccountCredentials> = (
|
||||||
|
rotationAccountCredentials: C,
|
||||||
|
currentCredentials: C
|
||||||
|
) => Promise<C>;
|
||||||
|
|
||||||
export type TPamResourceFactory<T extends TPamResourceConnectionDetails, C extends TPamAccountCredentials> = (
|
export type TPamResourceFactory<T extends TPamResourceConnectionDetails, C extends TPamAccountCredentials> = (
|
||||||
resourceType: PamResource,
|
resourceType: PamResource,
|
||||||
@@ -39,4 +50,5 @@ export type TPamResourceFactory<T extends TPamResourceConnectionDetails, C exten
|
|||||||
) => {
|
) => {
|
||||||
validateConnection: TPamResourceFactoryValidateConnection<T>;
|
validateConnection: TPamResourceFactoryValidateConnection<T>;
|
||||||
validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<C>;
|
validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<C>;
|
||||||
|
rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<C>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,13 +15,24 @@ import {
|
|||||||
BaseSqlResourceConnectionDetailsSchema
|
BaseSqlResourceConnectionDetailsSchema
|
||||||
} from "../shared/sql/sql-resource-schemas";
|
} from "../shared/sql/sql-resource-schemas";
|
||||||
|
|
||||||
// Resources
|
|
||||||
export const PostgresResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema;
|
export const PostgresResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema;
|
||||||
|
export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
|
||||||
|
|
||||||
|
// Resources
|
||||||
const BasePostgresResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.Postgres) });
|
const BasePostgresResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.Postgres) });
|
||||||
|
|
||||||
export const PostgresResourceSchema = BasePostgresResourceSchema.extend({
|
export const PostgresResourceSchema = BasePostgresResourceSchema.extend({
|
||||||
connectionDetails: PostgresResourceConnectionDetailsSchema
|
connectionDetails: PostgresResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedPostgresResourceSchema = BasePostgresResourceSchema.extend({
|
||||||
|
connectionDetails: PostgresResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: PostgresAccountCredentialsSchema.pick({
|
||||||
|
username: true
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PostgresResourceListItemSchema = z.object({
|
export const PostgresResourceListItemSchema = z.object({
|
||||||
@@ -30,16 +41,16 @@ export const PostgresResourceListItemSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({
|
export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||||
connectionDetails: PostgresResourceConnectionDetailsSchema
|
connectionDetails: PostgresResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({
|
export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||||
connectionDetails: PostgresResourceConnectionDetailsSchema.optional()
|
connectionDetails: PostgresResourceConnectionDetailsSchema.optional(),
|
||||||
|
rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
|
|
||||||
|
|
||||||
export const PostgresAccountSchema = BasePamAccountSchema.extend({
|
export const PostgresAccountSchema = BasePamAccountSchema.extend({
|
||||||
credentials: PostgresAccountCredentialsSchema
|
credentials: PostgresAccountCredentialsSchema
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import knex, { Knex } from "knex";
|
import knex from "knex";
|
||||||
|
import mysql, { Connection } from "mysql2/promise";
|
||||||
|
import * as pg from "pg";
|
||||||
import tls, { PeerCertificate } from "tls";
|
import tls, { PeerCertificate } from "tls";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||||
@@ -6,39 +8,174 @@ import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { GatewayProxyProtocol } from "@app/lib/gateway";
|
import { GatewayProxyProtocol } from "@app/lib/gateway";
|
||||||
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { PamResource } from "../../pam-resource-enums";
|
import { PamResource } from "../../pam-resource-enums";
|
||||||
import { TPamResourceFactory, TPamResourceFactoryValidateAccountCredentials } from "../../pam-resource-types";
|
import {
|
||||||
|
TPamResourceFactory,
|
||||||
|
TPamResourceFactoryRotateAccountCredentials,
|
||||||
|
TPamResourceFactoryValidateAccountCredentials
|
||||||
|
} from "../../pam-resource-types";
|
||||||
import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "./sql-resource-types";
|
import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "./sql-resource-types";
|
||||||
|
|
||||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
|
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
|
||||||
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
||||||
|
const SIMPLE_QUERY = "select 1";
|
||||||
|
|
||||||
const SQL_CONNECTION_CLIENT_MAP = {
|
export interface SqlResourceConnection {
|
||||||
[PamResource.Postgres]: "pg"
|
/**
|
||||||
};
|
* Check and see if the connection is good or not.
|
||||||
|
*
|
||||||
|
* @param connectOnly when true, if we only want to know that making the connection is possible or not,
|
||||||
|
* we don't care about authentication failures
|
||||||
|
* @returns Promise to be resolved when the connection is good, otherwise an error will be errbacked
|
||||||
|
*/
|
||||||
|
validate: (connectOnly: boolean) => Promise<void>;
|
||||||
|
|
||||||
const getConnectionConfig = (
|
/**
|
||||||
resourceType: PamResource,
|
* Rotate password and return the new credentials.
|
||||||
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails
|
*
|
||||||
) => {
|
* @param currentCredentials the current credentials to rotate
|
||||||
switch (resourceType) {
|
*
|
||||||
|
* @returns Promise to be resolved with the new credentials
|
||||||
|
*/
|
||||||
|
rotateCredentials: (currentCredentials: TSqlAccountCredentials) => Promise<TSqlAccountCredentials>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the connection.
|
||||||
|
*
|
||||||
|
* @returns Promise for closing the connection
|
||||||
|
*/
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeSqlConnection = (
|
||||||
|
proxyPort: number,
|
||||||
|
config: {
|
||||||
|
connectionDetails: TSqlResourceConnectionDetails;
|
||||||
|
resourceType: PamResource;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
): SqlResourceConnection => {
|
||||||
|
const { connectionDetails, resourceType, username, password } = config;
|
||||||
|
const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails;
|
||||||
|
const actualUsername = username ?? TEST_CONNECTION_USERNAME; // Use provided username or fallback
|
||||||
|
const actualPassword = password ?? TEST_CONNECTION_PASSWORD; // Use provided password or fallback
|
||||||
|
switch (config.resourceType) {
|
||||||
case PamResource.Postgres: {
|
case PamResource.Postgres: {
|
||||||
|
const client = knex({
|
||||||
|
client: "pg",
|
||||||
|
connection: {
|
||||||
|
host: "localhost",
|
||||||
|
port: proxyPort,
|
||||||
|
user: actualUsername,
|
||||||
|
password: actualPassword,
|
||||||
|
database: connectionDetails.database,
|
||||||
|
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
|
ssl: sslEnabled
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: sslRejectUnauthorized,
|
||||||
|
ca: sslCertificate,
|
||||||
|
servername: host,
|
||||||
|
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
||||||
|
// but validate the certificate against the actual hostname
|
||||||
|
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
||||||
|
return tls.checkServerIdentity(host, cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
ssl: sslEnabled
|
validate: async (connectOnly) => {
|
||||||
? {
|
try {
|
||||||
rejectUnauthorized: sslRejectUnauthorized,
|
await client.raw(SIMPLE_QUERY);
|
||||||
ca: sslCertificate,
|
} catch (error) {
|
||||||
servername: host,
|
if (error instanceof pg.DatabaseError) {
|
||||||
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
// Hacky way to know if we successfully hit the database.
|
||||||
// but validate the certificate against the actual hostname
|
// TODO: potentially two approaches to solve the problem.
|
||||||
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
// 1. change the work flow, add account first then resource
|
||||||
return tls.checkServerIdentity(host, cert);
|
// 2. modify relay to add a new endpoint for returning if the target host is healthy or not
|
||||||
|
// (like being able to do an auth handshake regardless pass or not)
|
||||||
|
if (
|
||||||
|
connectOnly &&
|
||||||
|
(error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ||
|
||||||
|
error.message.includes("no pg_hba.conf entry for host"))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: false
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rotateCredentials: async (currentCredentials) => {
|
||||||
|
const newPassword = alphaNumericNanoId(32);
|
||||||
|
// Note: The generated random password is not really going to make SQL Injection possible.
|
||||||
|
// The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL,
|
||||||
|
// parameters binding is not supported. But just in case if the this code got copied
|
||||||
|
// around and repurposed, let's just do some naive escaping regardless
|
||||||
|
await client.raw(`ALTER USER :username: WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`, {
|
||||||
|
username: currentCredentials.username
|
||||||
|
});
|
||||||
|
return { username: currentCredentials.username, password: newPassword };
|
||||||
|
},
|
||||||
|
close: () => client.destroy()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case PamResource.MySQL: {
|
||||||
|
return {
|
||||||
|
validate: async (connectOnly) => {
|
||||||
|
let client: Connection | null = null;
|
||||||
|
try {
|
||||||
|
// Notice: the reason we are not using Knex for mysql2 is because we don't need any fancy feature from Knex.
|
||||||
|
// mysql2 doesn't provide custom ssl verification function pass in.
|
||||||
|
// ref: https://github.com/sidorares/node-mysql2/blob/2543272a2ada8d8a07f74582549d7dd3fe948e2d/lib/base/connection.js#L358-L362
|
||||||
|
// and then even I tried to workaround it with Knex's pool afterCreate hook, but then encounter a bug:
|
||||||
|
// ref: https://github.com/knex/knex/issues/5352
|
||||||
|
// It appears that using Knex causing more troubles than not, we are just checking the connections,
|
||||||
|
// so it's much easier to create raw connection with the driver lib directly
|
||||||
|
client = await mysql.createConnection({
|
||||||
|
host: "localhost",
|
||||||
|
port: proxyPort,
|
||||||
|
user: actualUsername, // Use provided username or fallback
|
||||||
|
password: actualPassword, // Use provided password or fallback
|
||||||
|
database: connectionDetails.database,
|
||||||
|
ssl: sslEnabled
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: sslRejectUnauthorized,
|
||||||
|
ca: sslCertificate
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
await client.query(SIMPLE_QUERY);
|
||||||
|
} catch (error) {
|
||||||
|
if (connectOnly) {
|
||||||
|
// Hacky way to know if we successfully hit the database.
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.startsWith(`Access denied for user '${TEST_CONNECTION_USERNAME}'@`)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: handle other errors, and throw standardlized errors providing user-friendly msg
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await client?.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rotateCredentials: async () => {
|
||||||
|
// TODO: the pwd rotation for MySQL is not supported yet
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unsupported operation"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
close: async () => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -57,10 +194,9 @@ export const executeWithGateway = async <T>(
|
|||||||
password?: string;
|
password?: string;
|
||||||
},
|
},
|
||||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||||
operation: (client: Knex) => Promise<T>
|
operation: (connection: SqlResourceConnection) => Promise<T>
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const { connectionDetails, resourceType, gatewayId, username, password } = config;
|
const { connectionDetails, gatewayId } = config;
|
||||||
|
|
||||||
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
||||||
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||||
gatewayId,
|
gatewayId,
|
||||||
@@ -74,22 +210,11 @@ export const executeWithGateway = async <T>(
|
|||||||
|
|
||||||
return withGatewayV2Proxy(
|
return withGatewayV2Proxy(
|
||||||
async (proxyPort) => {
|
async (proxyPort) => {
|
||||||
const client = knex({
|
const connection = makeSqlConnection(proxyPort, config);
|
||||||
client: SQL_CONNECTION_CLIENT_MAP[resourceType],
|
|
||||||
connection: {
|
|
||||||
database: connectionDetails.database,
|
|
||||||
port: proxyPort,
|
|
||||||
host: "localhost",
|
|
||||||
user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback
|
|
||||||
password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback
|
|
||||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
...getConnectionConfig(resourceType, connectionDetails)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
return await operation(client);
|
return await operation(connection);
|
||||||
} finally {
|
} finally {
|
||||||
await client.destroy();
|
await connection.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -110,25 +235,14 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
const validateConnection = async () => {
|
const validateConnection = async () => {
|
||||||
try {
|
try {
|
||||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
||||||
await client.raw("Select 1");
|
await client.validate(true);
|
||||||
});
|
});
|
||||||
return connectionDetails;
|
return connectionDetails;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Hacky way to know if we successfully hit the database
|
if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") {
|
||||||
if (error instanceof BadRequestError) {
|
throw new BadRequestError({
|
||||||
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) {
|
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||||
return connectionDetails;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes("no pg_hba.conf entry for host")) {
|
|
||||||
return connectionDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message === "Connection terminated unexpectedly") {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -151,11 +265,12 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
},
|
},
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
async (client) => {
|
async (client) => {
|
||||||
await client.raw("Select 1");
|
await client.validate(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// TODO: extract these logic into each SQL connection
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -176,8 +291,52 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<TSqlAccountCredentials> = async (
|
||||||
|
rotationAccountCredentials,
|
||||||
|
currentCredentials
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await executeWithGateway(
|
||||||
|
{
|
||||||
|
connectionDetails,
|
||||||
|
gatewayId,
|
||||||
|
resourceType,
|
||||||
|
username: rotationAccountCredentials.username,
|
||||||
|
password: rotationAccountCredentials.password
|
||||||
|
},
|
||||||
|
gatewayV2Service,
|
||||||
|
(client) => client.rotateCredentials(currentCredentials)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BadRequestError) {
|
||||||
|
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Management credentials invalid: Username or password incorrect"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message.includes("permission denied")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Management credentials lack permission to rotate password for user "${currentCredentials.username}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message === "Connection terminated unexpectedly") {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to rotate account credentials for ${resourceType}: ${(error as Error).message || String(error)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validateConnection,
|
validateConnection,
|
||||||
validateAccountCredentials
|
validateAccountCredentials,
|
||||||
|
rotateAccountCredentials
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ export const BaseSqlResourceConnectionDetailsSchema = z.object({
|
|||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
export const BaseSqlAccountCredentialsSchema = z.object({
|
export const BaseSqlAccountCredentialsSchema = z.object({
|
||||||
username: z.string().trim().min(1),
|
username: z.string().trim().min(1).max(63),
|
||||||
password: z.string().trim().min(1)
|
password: z.string().trim().min(1).max(256)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { TMySQLAccountCredentials, TMySQLResourceConnectionDetails } from "../../mysql/mysql-resource-types";
|
||||||
import {
|
import {
|
||||||
TPostgresAccountCredentials,
|
TPostgresAccountCredentials,
|
||||||
TPostgresResourceConnectionDetails
|
TPostgresResourceConnectionDetails
|
||||||
} from "../../postgres/postgres-resource-types";
|
} from "../../postgres/postgres-resource-types";
|
||||||
|
|
||||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||||
export type TSqlAccountCredentials = TPostgresAccountCredentials;
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
|
export type TSqlAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ type TSamlConfigServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||||
membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
|
membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find" | "create">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const samlConfigServiceFactory = ({
|
export const samlConfigServiceFactory = ({
|
||||||
@@ -183,6 +183,22 @@ export const samlConfigServiceFactory = ({
|
|||||||
transaction
|
transaction
|
||||||
);
|
);
|
||||||
orgGroupsMap.set(groupName, newGroup);
|
orgGroupsMap.set(groupName, newGroup);
|
||||||
|
const orgMembership = await membershipGroupDAL.create(
|
||||||
|
{
|
||||||
|
actorGroupId: newGroup.id,
|
||||||
|
scope: AccessScope.Organization,
|
||||||
|
scopeOrgId: orgId
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
await membershipRoleDAL.create(
|
||||||
|
{
|
||||||
|
membershipId: orgMembership.id,
|
||||||
|
role: OrgMembershipRole.NoAccess,
|
||||||
|
customRoleId: null
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { QueueWorkerProfile } from "@app/lib/types";
|
import { QueueWorkerProfile } from "@app/lib/types";
|
||||||
|
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
@@ -363,11 +365,6 @@ const envSchema = z
|
|||||||
/* INTERNAL ----------------------------------------------------------------------------- */
|
/* INTERNAL ----------------------------------------------------------------------------- */
|
||||||
INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional())
|
INTERNAL_REGION: zpStr(z.enum(["us", "eu"]).optional())
|
||||||
})
|
})
|
||||||
// To ensure that basic encryption is always possible.
|
|
||||||
.refine(
|
|
||||||
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
|
|
||||||
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
|
|
||||||
)
|
|
||||||
.refine(
|
.refine(
|
||||||
(data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS) || Boolean(data.REDIS_CLUSTER_HOSTS),
|
(data) => Boolean(data.REDIS_URL) || Boolean(data.REDIS_SENTINEL_HOSTS) || Boolean(data.REDIS_CLUSTER_HOSTS),
|
||||||
"Either REDIS_URL, REDIS_SENTINEL_HOSTS or REDIS_CLUSTER_HOSTS must be defined."
|
"Either REDIS_URL, REDIS_SENTINEL_HOSTS or REDIS_CLUSTER_HOSTS must be defined."
|
||||||
@@ -453,7 +450,12 @@ export const getConfig = () => envCfg;
|
|||||||
export const getOriginalConfig = () => originalEnvConfig;
|
export const getOriginalConfig = () => originalEnvConfig;
|
||||||
|
|
||||||
// cannot import singleton logger directly as it needs config to load various transport
|
// cannot import singleton logger directly as it needs config to load various transport
|
||||||
export const initEnvConfig = async (superAdminDAL?: TSuperAdminDALFactory, logger?: CustomLogger) => {
|
export const initEnvConfig = async (
|
||||||
|
hsmService: THsmServiceFactory,
|
||||||
|
kmsRootConfigDAL: TKmsRootConfigDALFactory,
|
||||||
|
superAdminDAL?: TSuperAdminDALFactory,
|
||||||
|
logger?: CustomLogger
|
||||||
|
) => {
|
||||||
const parsedEnv = envSchema.safeParse(process.env);
|
const parsedEnv = envSchema.safeParse(process.env);
|
||||||
if (!parsedEnv.success) {
|
if (!parsedEnv.success) {
|
||||||
(logger ?? console).error("Invalid environment variables. Check the error below");
|
(logger ?? console).error("Invalid environment variables. Check the error below");
|
||||||
@@ -469,7 +471,7 @@ export const initEnvConfig = async (superAdminDAL?: TSuperAdminDALFactory, logge
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (superAdminDAL) {
|
if (superAdminDAL) {
|
||||||
const fipsEnabled = await crypto.initialize(superAdminDAL);
|
const fipsEnabled = await crypto.initialize(superAdminDAL, hsmService, kmsRootConfigDAL);
|
||||||
|
|
||||||
if (fipsEnabled) {
|
if (fipsEnabled) {
|
||||||
const newEnvCfg = {
|
const newEnvCfg = {
|
||||||
@@ -532,6 +534,22 @@ export const getDatabaseCredentials = (logger?: CustomLogger) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getHsmConfig = (logger?: CustomLogger) => {
|
||||||
|
const parsedEnv = envSchema.safeParse(process.env);
|
||||||
|
if (!parsedEnv.success) {
|
||||||
|
(logger ?? console).error("Invalid environment variables. Check the error below");
|
||||||
|
(logger ?? console).error(parsedEnv.error.issues);
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isHsmConfigured: parsedEnv.data.isHsmConfigured,
|
||||||
|
HSM_PIN: parsedEnv.data.HSM_PIN,
|
||||||
|
HSM_SLOT: parsedEnv.data.HSM_SLOT,
|
||||||
|
HSM_LIB_PATH: parsedEnv.data.HSM_LIB_PATH,
|
||||||
|
HSM_KEY_LABEL: parsedEnv.data.HSM_KEY_LABEL
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// A list of environment variables that can be overwritten
|
// A list of environment variables that can be overwritten
|
||||||
export const overwriteSchema: {
|
export const overwriteSchema: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import nacl from "tweetnacl";
|
|||||||
import naclUtils from "tweetnacl-util";
|
import naclUtils from "tweetnacl-util";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { isHsmActiveAndEnabled } from "@app/ee/services/hsm/hsm-fns";
|
||||||
|
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service";
|
import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
@@ -106,49 +110,73 @@ const cryptographyFactory = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
|
const $setFipsModeEnabled = async (
|
||||||
|
enabled: boolean,
|
||||||
|
hsmService: THsmServiceFactory,
|
||||||
|
kmsRootConfigDAL: TKmsRootConfigDALFactory,
|
||||||
|
envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">
|
||||||
|
) => {
|
||||||
// If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key.
|
// If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key.
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
crypto.setFips(true);
|
crypto.setFips(true);
|
||||||
|
|
||||||
const appCfg = envCfg || getConfig();
|
const appCfg = envCfg || getConfig();
|
||||||
|
|
||||||
if (appCfg.ENCRYPTION_KEY) {
|
const hsmStatus = await isHsmActiveAndEnabled({
|
||||||
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
|
hsmService,
|
||||||
|
kmsRootConfigDAL
|
||||||
|
});
|
||||||
|
|
||||||
// note(daniel): for some reason this resolves as true for some hex-encoded strings.
|
// if the encryption strategy is software - user needs to provide an encryption key
|
||||||
if (!isBase64(appCfg.ENCRYPTION_KEY)) {
|
// if the encryption strategy is null AND the hsm is not configured - user needs to provide an encryption key
|
||||||
|
const needsEncryptionKey =
|
||||||
|
hsmStatus.rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.Software ||
|
||||||
|
(hsmStatus.rootKmsConfigEncryptionStrategy === null && !hsmStatus.isHsmConfigured);
|
||||||
|
|
||||||
|
// only perform encryption key validation if it's actually required.
|
||||||
|
if (needsEncryptionKey) {
|
||||||
|
if (appCfg.ENCRYPTION_KEY) {
|
||||||
|
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
|
||||||
|
|
||||||
|
// note(daniel): for some reason this resolves as true for some hex-encoded strings.
|
||||||
|
if (!isBase64(appCfg.ENCRYPTION_KEY)) {
|
||||||
|
throw new CryptographyError({
|
||||||
|
message:
|
||||||
|
"FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a base64 encoded 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesToBits(Buffer.from(appCfg.ENCRYPTION_KEY, "base64").length) !== 256) {
|
||||||
|
throw new CryptographyError({
|
||||||
|
message:
|
||||||
|
"FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
throw new CryptographyError({
|
throw new CryptographyError({
|
||||||
message:
|
message:
|
||||||
"FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a base64 encoded 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`"
|
"FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not set.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytesToBits(Buffer.from(appCfg.ENCRYPTION_KEY, "base64").length) !== 256) {
|
|
||||||
throw new CryptographyError({
|
|
||||||
message:
|
|
||||||
"FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new CryptographyError({
|
|
||||||
message:
|
|
||||||
"FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not set.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$fipsEnabled = enabled;
|
$fipsEnabled = enabled;
|
||||||
$isInitialized = true;
|
$isInitialized = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
|
const initialize = async (
|
||||||
|
superAdminDAL: TSuperAdminDALFactory,
|
||||||
|
hsmService: THsmServiceFactory,
|
||||||
|
kmsRootConfigDAL: TKmsRootConfigDALFactory,
|
||||||
|
envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">
|
||||||
|
) => {
|
||||||
if ($isInitialized) {
|
if ($isInitialized) {
|
||||||
return isFipsModeEnabled();
|
return isFipsModeEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.FIPS_ENABLED !== "true") {
|
if (process.env.FIPS_ENABLED !== "true") {
|
||||||
logger.info("Cryptography module initialized in normal operation mode.");
|
logger.info("Cryptography module initialized in normal operation mode.");
|
||||||
$setFipsModeEnabled(false, envCfg);
|
await $setFipsModeEnabled(false, hsmService, kmsRootConfigDAL, envCfg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +186,11 @@ const cryptographyFactory = () => {
|
|||||||
if (serverCfg) {
|
if (serverCfg) {
|
||||||
if (serverCfg.fipsEnabled) {
|
if (serverCfg.fipsEnabled) {
|
||||||
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
|
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
|
||||||
$setFipsModeEnabled(true, envCfg);
|
await $setFipsModeEnabled(true, hsmService, kmsRootConfigDAL, envCfg);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
|
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
|
||||||
$setFipsModeEnabled(false, envCfg);
|
await $setFipsModeEnabled(false, hsmService, kmsRootConfigDAL, envCfg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +199,7 @@ const cryptographyFactory = () => {
|
|||||||
// TODO(daniel): check if it's an enterprise deployment
|
// TODO(daniel): check if it's an enterprise deployment
|
||||||
|
|
||||||
// if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true.
|
// if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true.
|
||||||
$setFipsModeEnabled(true, envCfg);
|
await $setFipsModeEnabled(true, hsmService, kmsRootConfigDAL, envCfg);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,6 +286,13 @@ const cryptographyFactory = () => {
|
|||||||
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
||||||
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if (!rootEncryptionKey && !encryptionKey) {
|
||||||
|
throw new CryptographyError({
|
||||||
|
message: "Tried to encrypt with instance root encryption key, but no root encryption key is set."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (rootEncryptionKey) {
|
if (rootEncryptionKey) {
|
||||||
const { iv, tag, ciphertext } = encrypt({
|
const { iv, tag, ciphertext } = encrypt({
|
||||||
plaintext: data,
|
plaintext: data,
|
||||||
@@ -303,6 +338,14 @@ const cryptographyFactory = () => {
|
|||||||
// the or gate is used used in migration
|
// the or gate is used used in migration
|
||||||
const rootEncryptionKey = appCfg?.ROOT_ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY;
|
const rootEncryptionKey = appCfg?.ROOT_ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY;
|
||||||
const encryptionKey = appCfg?.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
|
const encryptionKey = appCfg?.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if (!rootEncryptionKey && !encryptionKey) {
|
||||||
|
throw new CryptographyError({
|
||||||
|
message: "Tried to decrypt with instance root encryption key, but no root encryption key is set."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
|
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
|
||||||
const data = symmetric().decrypt({
|
const data = symmetric().decrypt({
|
||||||
key: rootEncryptionKey,
|
key: rootEncryptionKey,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import https from "https";
|
|||||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||||
import { splitPemChain } from "@app/services/certificate/certificate-fns";
|
import { splitPemChain } from "@app/services/certificate/certificate-fns";
|
||||||
|
|
||||||
|
import { getConfig } from "../config/env";
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
import { GatewayProxyProtocol } from "../gateway/types";
|
import { GatewayProxyProtocol } from "../gateway/types";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
@@ -80,6 +81,8 @@ const createGatewayConnection = async (
|
|||||||
gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string },
|
gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string },
|
||||||
protocol: GatewayProxyProtocol
|
protocol: GatewayProxyProtocol
|
||||||
): Promise<net.Socket> => {
|
): Promise<net.Socket> => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const protocolToAlpn = {
|
const protocolToAlpn = {
|
||||||
[GatewayProxyProtocol.Http]: "infisical-http-proxy",
|
[GatewayProxyProtocol.Http]: "infisical-http-proxy",
|
||||||
[GatewayProxyProtocol.Tcp]: "infisical-tcp-proxy",
|
[GatewayProxyProtocol.Tcp]: "infisical-tcp-proxy",
|
||||||
@@ -94,7 +97,8 @@ const createGatewayConnection = async (
|
|||||||
minVersion: "TLSv1.2",
|
minVersion: "TLSv1.2",
|
||||||
maxVersion: "TLSv1.3",
|
maxVersion: "TLSv1.3",
|
||||||
rejectUnauthorized: true,
|
rejectUnauthorized: true,
|
||||||
ALPNProtocols: [protocolToAlpn[protocol]]
|
ALPNProtocols: [protocolToAlpn[protocol]],
|
||||||
|
checkServerIdentity: appCfg.isDevelopmentMode ? () => undefined : tls.checkServerIdentity
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
+19
-6
@@ -9,14 +9,16 @@ import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
|||||||
|
|
||||||
import { runMigrations } from "./auto-start-migrations";
|
import { runMigrations } from "./auto-start-migrations";
|
||||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||||
|
import { hsmServiceFactory } from "./ee/services/hsm/hsm-service";
|
||||||
import { keyStoreFactory } from "./keystore/keystore";
|
import { keyStoreFactory } from "./keystore/keystore";
|
||||||
import { formatSmtpConfig, getDatabaseCredentials, initEnvConfig } from "./lib/config/env";
|
import { formatSmtpConfig, getDatabaseCredentials, getHsmConfig, initEnvConfig } from "./lib/config/env";
|
||||||
import { buildRedisFromConfig } from "./lib/config/redis";
|
import { buildRedisFromConfig } from "./lib/config/redis";
|
||||||
import { removeTemporaryBaseDirectory } from "./lib/files";
|
import { removeTemporaryBaseDirectory } from "./lib/files";
|
||||||
import { initLogger } from "./lib/logger";
|
import { initLogger } from "./lib/logger";
|
||||||
import { queueServiceFactory } from "./queue";
|
import { queueServiceFactory } from "./queue";
|
||||||
import { main } from "./server/app";
|
import { main } from "./server/app";
|
||||||
import { bootstrapCheck } from "./server/boot-strap-check";
|
import { bootstrapCheck } from "./server/boot-strap-check";
|
||||||
|
import { kmsRootConfigDALFactory } from "./services/kms/kms-root-config-dal";
|
||||||
import { smtpServiceFactory } from "./services/smtp/smtp-service";
|
import { smtpServiceFactory } from "./services/smtp/smtp-service";
|
||||||
import { superAdminDALFactory } from "./services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "./services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
@@ -26,6 +28,18 @@ const run = async () => {
|
|||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
await removeTemporaryBaseDirectory();
|
await removeTemporaryBaseDirectory();
|
||||||
|
|
||||||
|
const hsmConfig = getHsmConfig(logger);
|
||||||
|
|
||||||
|
const hsmModule = initializeHsmModule(hsmConfig);
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
hsmModule: hsmModule.getModule(),
|
||||||
|
envConfig: hsmConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
await hsmService.startService();
|
||||||
|
|
||||||
const databaseCredentials = getDatabaseCredentials(logger);
|
const databaseCredentials = getDatabaseCredentials(logger);
|
||||||
|
|
||||||
const db = initDbConnection({
|
const db = initDbConnection({
|
||||||
@@ -35,7 +49,8 @@ const run = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const superAdminDAL = superAdminDALFactory(db);
|
const superAdminDAL = superAdminDALFactory(db);
|
||||||
const envConfig = await initEnvConfig(superAdminDAL, logger);
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
|
const envConfig = await initEnvConfig(hsmService, kmsRootConfigDAL, superAdminDAL, logger);
|
||||||
|
|
||||||
const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI
|
const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI
|
||||||
? initAuditLogDbConnection({
|
? initAuditLogDbConnection({
|
||||||
@@ -59,14 +74,12 @@ const run = async () => {
|
|||||||
const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL);
|
const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL);
|
||||||
const redis = buildRedisFromConfig(envConfig);
|
const redis = buildRedisFromConfig(envConfig);
|
||||||
|
|
||||||
const hsmModule = initializeHsmModule(envConfig);
|
|
||||||
hsmModule.initialize();
|
|
||||||
|
|
||||||
const server = await main({
|
const server = await main({
|
||||||
db,
|
db,
|
||||||
auditLogDb,
|
auditLogDb,
|
||||||
superAdminDAL,
|
superAdminDAL,
|
||||||
hsmModule: hsmModule.getModule(),
|
kmsRootConfigDAL,
|
||||||
|
hsmService,
|
||||||
smtp,
|
smtp,
|
||||||
logger,
|
logger,
|
||||||
queue,
|
queue,
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ export enum QueueName {
|
|||||||
DailyReminders = "daily-reminders",
|
DailyReminders = "daily-reminders",
|
||||||
SecretReminderMigration = "secret-reminder-migration",
|
SecretReminderMigration = "secret-reminder-migration",
|
||||||
UserNotification = "user-notification",
|
UserNotification = "user-notification",
|
||||||
HealthAlert = "health-alert"
|
HealthAlert = "health-alert",
|
||||||
|
CertificateV3AutoRenewal = "certificate-v3-auto-renewal",
|
||||||
|
PamAccountRotation = "pam-account-rotation"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -126,7 +128,9 @@ export enum QueueJobs {
|
|||||||
DailyReminders = "daily-reminders",
|
DailyReminders = "daily-reminders",
|
||||||
SecretReminderMigration = "secret-reminder-migration",
|
SecretReminderMigration = "secret-reminder-migration",
|
||||||
UserNotification = "user-notification-job",
|
UserNotification = "user-notification-job",
|
||||||
HealthAlert = "health-alert"
|
HealthAlert = "health-alert",
|
||||||
|
CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal",
|
||||||
|
PamAccountRotation = "pam-account-rotation"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@@ -357,6 +361,14 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.HealthAlert;
|
name: QueueJobs.HealthAlert;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
};
|
};
|
||||||
|
[QueueName.CertificateV3AutoRenewal]: {
|
||||||
|
name: QueueJobs.CertificateV3DailyAutoRenewal;
|
||||||
|
payload: undefined;
|
||||||
|
};
|
||||||
|
[QueueName.PamAccountRotation]: {
|
||||||
|
name: QueueJobs.PamAccountRotation;
|
||||||
|
payload: undefined;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const SECRET_SCANNING_JOBS = [
|
const SECRET_SCANNING_JOBS = [
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ import fastify from "fastify";
|
|||||||
import { Cluster, Redis } from "ioredis";
|
import { Cluster, Redis } from "ioredis";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
|
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig, IS_PACKAGED, TEnvConfig } from "@app/lib/config/env";
|
import { getConfig, IS_PACKAGED, TEnvConfig } from "@app/lib/config/env";
|
||||||
import { CustomLogger } from "@app/lib/logger/logger";
|
import { CustomLogger } from "@app/lib/logger/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
|
||||||
@@ -42,16 +43,16 @@ type TMain = {
|
|||||||
logger?: CustomLogger;
|
logger?: CustomLogger;
|
||||||
queue: TQueueServiceFactory;
|
queue: TQueueServiceFactory;
|
||||||
keyStore: TKeyStoreFactory;
|
keyStore: TKeyStoreFactory;
|
||||||
hsmModule: HsmModule;
|
|
||||||
redis: Redis | Cluster;
|
redis: Redis | Cluster;
|
||||||
envConfig: TEnvConfig;
|
envConfig: TEnvConfig;
|
||||||
superAdminDAL: TSuperAdminDALFactory;
|
superAdminDAL: TSuperAdminDALFactory;
|
||||||
|
hsmService: THsmServiceFactory;
|
||||||
|
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run the server!
|
// Run the server!
|
||||||
export const main = async ({
|
export const main = async ({
|
||||||
db,
|
db,
|
||||||
hsmModule,
|
|
||||||
auditLogDb,
|
auditLogDb,
|
||||||
smtp,
|
smtp,
|
||||||
logger,
|
logger,
|
||||||
@@ -59,7 +60,9 @@ export const main = async ({
|
|||||||
keyStore,
|
keyStore,
|
||||||
redis,
|
redis,
|
||||||
envConfig,
|
envConfig,
|
||||||
superAdminDAL
|
superAdminDAL,
|
||||||
|
hsmService,
|
||||||
|
kmsRootConfigDAL
|
||||||
}: TMain) => {
|
}: TMain) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -148,9 +151,10 @@ export const main = async ({
|
|||||||
db,
|
db,
|
||||||
auditLogDb,
|
auditLogDb,
|
||||||
keyStore,
|
keyStore,
|
||||||
hsmModule,
|
hsmService,
|
||||||
envConfig,
|
envConfig,
|
||||||
superAdminDAL
|
superAdminDAL,
|
||||||
|
kmsRootConfigDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.register(registerServeUI, {
|
await server.register(registerServeUI, {
|
||||||
|
|||||||
@@ -43,6 +43,6 @@ export const GenericResourceNameSchema = z
|
|||||||
export const BaseSecretNameSchema = z.string().trim().min(1);
|
export const BaseSecretNameSchema = z.string().trim().min(1);
|
||||||
|
|
||||||
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||||
(el) => !el.includes(":"),
|
(el) => !el.includes(":") && !el.includes("/"),
|
||||||
"Secret name cannot contain colon."
|
"Secret name cannot contain colon or forward slash."
|
||||||
).refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
|
);
|
||||||
|
|||||||
@@ -138,6 +138,11 @@ export const injectIdentity = fp(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authentication is handled on a route-level
|
||||||
|
if (req.url === "/api/v1/relays/heartbeat-instance-relay") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication is handled on a route-level here.
|
// Authentication is handled on a route-level here.
|
||||||
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/gi
|
|||||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
import { isHsmActiveAndEnabled } from "@app/ee/services/hsm/hsm-fns";
|
||||||
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
|
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
|
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
|
||||||
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
|
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
|
||||||
import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal";
|
import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal";
|
||||||
@@ -138,6 +138,7 @@ import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
|||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig, TEnvConfig } from "@app/lib/config/env";
|
import { getConfig, TEnvConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -176,6 +177,7 @@ import { certificateTemplateEstConfigDALFactory } from "@app/services/certificat
|
|||||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
|
import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
|
||||||
import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
import { certificateV3QueueServiceFactory } from "@app/services/certificate-v3/certificate-v3-queue";
|
||||||
import { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
import { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
||||||
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
|
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||||
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
|
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
|
||||||
@@ -236,8 +238,9 @@ import { integrationAuthDALFactory } from "@app/services/integration-auth/integr
|
|||||||
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
import { membershipDALFactory } from "@app/services/membership/membership-dal";
|
import { membershipDALFactory } from "@app/services/membership/membership-dal";
|
||||||
import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
|
import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
|
||||||
import { membershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal";
|
import { membershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal";
|
||||||
@@ -255,11 +258,11 @@ import { userNotificationDALFactory } from "@app/services/notification/user-noti
|
|||||||
import { offlineUsageReportDALFactory } from "@app/services/offline-usage-report/offline-usage-report-dal";
|
import { offlineUsageReportDALFactory } from "@app/services/offline-usage-report/offline-usage-report-dal";
|
||||||
import { offlineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service";
|
import { offlineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service";
|
||||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||||
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
|
import { pamAccountRotationServiceFactory } from "@app/services/pam-account-rotation/pam-account-rotation-queue";
|
||||||
import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue";
|
import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue";
|
||||||
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
|
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
|
||||||
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||||
@@ -364,20 +367,22 @@ export const registerRoutes = async (
|
|||||||
auditLogDb,
|
auditLogDb,
|
||||||
superAdminDAL,
|
superAdminDAL,
|
||||||
db,
|
db,
|
||||||
hsmModule,
|
|
||||||
smtp: smtpService,
|
smtp: smtpService,
|
||||||
queue: queueService,
|
queue: queueService,
|
||||||
keyStore,
|
keyStore,
|
||||||
envConfig
|
envConfig,
|
||||||
|
hsmService,
|
||||||
|
kmsRootConfigDAL
|
||||||
}: {
|
}: {
|
||||||
auditLogDb?: Knex;
|
auditLogDb?: Knex;
|
||||||
superAdminDAL: TSuperAdminDALFactory;
|
superAdminDAL: TSuperAdminDALFactory;
|
||||||
db: Knex;
|
db: Knex;
|
||||||
hsmModule: HsmModule;
|
|
||||||
smtp: TSmtpService;
|
smtp: TSmtpService;
|
||||||
queue: TQueueServiceFactory;
|
queue: TQueueServiceFactory;
|
||||||
keyStore: TKeyStoreFactory;
|
keyStore: TKeyStoreFactory;
|
||||||
envConfig: TEnvConfig;
|
envConfig: TEnvConfig;
|
||||||
|
hsmService: THsmServiceFactory;
|
||||||
|
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -392,7 +397,6 @@ export const registerRoutes = async (
|
|||||||
const authTokenDAL = tokenDALFactory(db);
|
const authTokenDAL = tokenDALFactory(db);
|
||||||
const orgDAL = orgDALFactory(db);
|
const orgDAL = orgDALFactory(db);
|
||||||
const orgMembershipDAL = orgMembershipDALFactory(db);
|
const orgMembershipDAL = orgMembershipDALFactory(db);
|
||||||
const orgBotDAL = orgBotDALFactory(db);
|
|
||||||
const incidentContactDAL = incidentContactDALFactory(db);
|
const incidentContactDAL = incidentContactDALFactory(db);
|
||||||
const rateLimitDAL = rateLimitDALFactory(db);
|
const rateLimitDAL = rateLimitDALFactory(db);
|
||||||
const apiKeyDAL = apiKeyDALFactory(db);
|
const apiKeyDAL = apiKeyDALFactory(db);
|
||||||
@@ -509,7 +513,6 @@ export const registerRoutes = async (
|
|||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
const externalKmsDAL = externalKmsDALFactory(db);
|
const externalKmsDAL = externalKmsDALFactory(db);
|
||||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
|
||||||
|
|
||||||
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
const slackIntegrationDAL = slackIntegrationDALFactory(db);
|
||||||
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||||
@@ -569,7 +572,8 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
licenseDAL,
|
licenseDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
envConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL, orgDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL, orgDAL });
|
||||||
@@ -624,11 +628,6 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const hsmService = hsmServiceFactory({
|
|
||||||
hsmModule,
|
|
||||||
envConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
const kmsService = kmsServiceFactory({
|
const kmsService = kmsServiceFactory({
|
||||||
kmsRootConfigDAL,
|
kmsRootConfigDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -901,7 +900,6 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL,
|
|
||||||
oidcConfigDAL,
|
oidcConfigDAL,
|
||||||
ldapConfigDAL,
|
ldapConfigDAL,
|
||||||
loginService,
|
loginService,
|
||||||
@@ -2141,6 +2139,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const certificateV3Service = certificateV3ServiceFactory({
|
const certificateV3Service = certificateV3ServiceFactory({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
@@ -2148,6 +2147,13 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
|
queueService,
|
||||||
|
certificateDAL,
|
||||||
|
certificateV3Service,
|
||||||
|
auditLogService
|
||||||
|
});
|
||||||
|
|
||||||
const certificateEstV3Service = certificateEstV3ServiceFactory({
|
const certificateEstV3Service = certificateEstV3ServiceFactory({
|
||||||
internalCertificateAuthorityService,
|
internalCertificateAuthorityService,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
@@ -2264,7 +2270,13 @@ export const registerRoutes = async (
|
|||||||
pamSessionDAL,
|
pamSessionDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
userDAL
|
userDAL,
|
||||||
|
auditLogService
|
||||||
|
});
|
||||||
|
|
||||||
|
const pamAccountRotation = pamAccountRotationServiceFactory({
|
||||||
|
queueService,
|
||||||
|
pamAccountService
|
||||||
});
|
});
|
||||||
|
|
||||||
const pamSessionService = pamSessionServiceFactory({
|
const pamSessionService = pamSessionServiceFactory({
|
||||||
@@ -2298,16 +2310,39 @@ export const registerRoutes = async (
|
|||||||
// Start HSM service if it's configured/enabled.
|
// Start HSM service if it's configured/enabled.
|
||||||
await hsmService.startService();
|
await hsmService.startService();
|
||||||
|
|
||||||
|
const hsmStatus = await isHsmActiveAndEnabled({
|
||||||
|
hsmService,
|
||||||
|
kmsRootConfigDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the encryption strategy is software - user needs to provide an encryption key
|
||||||
|
// if the encryption strategy is null AND the hsm is not configured - user needs to provide an encryption key
|
||||||
|
const needsEncryptionKey =
|
||||||
|
hsmStatus.rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.Software ||
|
||||||
|
(hsmStatus.rootKmsConfigEncryptionStrategy === null && !hsmStatus.isHsmConfigured);
|
||||||
|
|
||||||
|
if (needsEncryptionKey) {
|
||||||
|
if (!envConfig.ROOT_ENCRYPTION_KEY && !envConfig.ENCRYPTION_KEY) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Root KMS encryption strategy is set to software. Please set the ENCRYPTION_KEY environment variable and restart your deployment.\nYou can enable HSM encryption in the Server Console."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await telemetryQueue.startTelemetryCheck();
|
await telemetryQueue.startTelemetryCheck();
|
||||||
await telemetryQueue.startAggregatedEventsJob();
|
await telemetryQueue.startAggregatedEventsJob();
|
||||||
await dailyResourceCleanUp.init();
|
await dailyResourceCleanUp.init();
|
||||||
await healthAlert.init();
|
await healthAlert.init();
|
||||||
await pkiSyncCleanup.init();
|
await pkiSyncCleanup.init();
|
||||||
|
await pamAccountRotation.init();
|
||||||
await dailyReminderQueueService.startDailyRemindersJob();
|
await dailyReminderQueueService.startDailyRemindersJob();
|
||||||
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
||||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||||
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
||||||
await kmsService.startService();
|
await certificateV3Queue.init();
|
||||||
|
await kmsService.startService(hsmStatus);
|
||||||
await microsoftTeamsService.start();
|
await microsoftTeamsService.start();
|
||||||
await dynamicSecretQueueService.init();
|
await dynamicSecretQueueService.init();
|
||||||
await eventBusService.init();
|
await eventBusService.init();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
apiConfig: z
|
apiConfig: z
|
||||||
.object({
|
.object({
|
||||||
autoRenew: z.boolean().default(false),
|
autoRenew: z.boolean().default(false),
|
||||||
autoRenewDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
@@ -150,7 +150,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
autoRenewDays: z.number().optional()
|
renewBeforeDays: z.number().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
}).array(),
|
}).array(),
|
||||||
@@ -230,7 +230,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
autoRenewDays: z.number().optional()
|
renewBeforeDays: z.number().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
metrics: z
|
metrics: z
|
||||||
@@ -355,7 +355,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
apiConfig: z
|
apiConfig: z
|
||||||
.object({
|
.object({
|
||||||
autoRenew: z.boolean().default(false),
|
autoRenew: z.boolean().default(false),
|
||||||
autoRenewDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1200,7 +1200,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificates: z.array(CertificatesSchema),
|
certificates: z.array(CertificatesSchema.extend({ hasPrivateKey: z.boolean() })),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
CertKeyUsageType,
|
CertKeyUsageType,
|
||||||
CertSubjectAlternativeNameType
|
CertSubjectAlternativeNameType
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
|
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
|
||||||
@@ -84,8 +85,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -169,9 +170,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
.min(1, "TTL cannot be empty")
|
.min(1, "TTL cannot be empty")
|
||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional()
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -192,6 +191,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
|
||||||
|
|
||||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -203,9 +204,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
ttl: req.body.ttl
|
ttl: req.body.ttl
|
||||||
},
|
},
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined
|
||||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
|
||||||
keyAlgorithm: req.body.keyAlgorithm
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -217,7 +216,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
certificateId: data.certificateId,
|
certificateId: data.certificateId,
|
||||||
profileName: data.profileName,
|
profileName: data.profileName,
|
||||||
commonName: ""
|
commonName: certificateRequest.commonName || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -260,8 +259,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional(),
|
||||||
commonName: validateTemplateRegexField.optional(),
|
commonName: validateTemplateRegexField.optional(),
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -343,4 +342,145 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:certificateId/renew",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
params: z.object({
|
||||||
|
certificateId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim(),
|
||||||
|
issuingCaCertificate: z.string().trim(),
|
||||||
|
certificateChain: z.string().trim(),
|
||||||
|
privateKey: z.string().trim().optional(),
|
||||||
|
serialNumber: z.string().trim(),
|
||||||
|
certificateId: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.certificateV3.renewCertificate({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
certificateId: req.params.certificateId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: data.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.RENEW_CERTIFICATE,
|
||||||
|
metadata: {
|
||||||
|
originalCertificateId: req.params.certificateId,
|
||||||
|
newCertificateId: data.certificateId,
|
||||||
|
profileName: data.profileName,
|
||||||
|
commonName: data.commonName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:certificateId/config",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
params: z.object({
|
||||||
|
certificateId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
renewBeforeDays: z.number().int().min(1).max(30).optional(),
|
||||||
|
enableAutoRenewal: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.refine((data) => !(data.renewBeforeDays !== undefined && data.enableAutoRenewal === false), {
|
||||||
|
message: "Cannot specify both renewBeforeDays and enableAutoRenewal=false"
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
renewBeforeDays: z.number().optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.body.enableAutoRenewal === false) {
|
||||||
|
const data = await server.services.certificateV3.disableRenewalConfig({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
certificateId: req.params.certificateId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: data.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG,
|
||||||
|
metadata: {
|
||||||
|
certificateId: req.params.certificateId,
|
||||||
|
commonName: data.commonName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Auto-renewal disabled successfully"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.renewBeforeDays !== undefined) {
|
||||||
|
const data = await server.services.certificateV3.updateRenewalConfig({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
certificateId: req.params.certificateId,
|
||||||
|
renewBeforeDays: req.body.renewBeforeDays
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: data.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG,
|
||||||
|
metadata: {
|
||||||
|
certificateId: req.params.certificateId,
|
||||||
|
renewBeforeDays: req.body.renewBeforeDays.toString(),
|
||||||
|
commonName: data.commonName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Certificate configuration updated successfully",
|
||||||
|
renewBeforeDays: data.renewBeforeDays
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "No configuration changes requested"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -345,6 +345,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case GitHubConnectionMethod.App:
|
case GitHubConnectionMethod.App:
|
||||||
case GitHubRadarConnectionMethod.App:
|
case GitHubRadarConnectionMethod.App:
|
||||||
return "GitHub App";
|
return "GitHub App";
|
||||||
|
case GitHubConnectionMethod.Pat:
|
||||||
|
return "Personal Access Token";
|
||||||
case AzureKeyVaultConnectionMethod.OAuth:
|
case AzureKeyVaultConnectionMethod.OAuth:
|
||||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum GitHubConnectionMethod {
|
export enum GitHubConnectionMethod {
|
||||||
OAuth = "oauth",
|
OAuth = "oauth",
|
||||||
App = "github-app"
|
App = "github-app",
|
||||||
|
Pat = "pat"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,10 +248,18 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
|||||||
): Promise<T[]> => {
|
): Promise<T[]> => {
|
||||||
const { credentials, method } = appConnection;
|
const { credentials, method } = appConnection;
|
||||||
|
|
||||||
const token =
|
let token: string;
|
||||||
method === GitHubConnectionMethod.OAuth
|
|
||||||
? credentials.accessToken
|
switch (method) {
|
||||||
: await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service);
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
token = credentials.accessToken;
|
||||||
|
break;
|
||||||
|
case GitHubConnectionMethod.Pat:
|
||||||
|
token = credentials.personalAccessToken;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
token = await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service);
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||||
const initialUrlObj = new URL(baseUrl);
|
const initialUrlObj = new URL(baseUrl);
|
||||||
@@ -460,6 +468,35 @@ export const validateGitHubConnectionCredentials = async (
|
|||||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
|
||||||
) => {
|
) => {
|
||||||
const { credentials, method } = config;
|
const { credentials, method } = config;
|
||||||
|
|
||||||
|
// PAT validation
|
||||||
|
if (method === GitHubConnectionMethod.Pat) {
|
||||||
|
try {
|
||||||
|
const apiUrl = await getGitHubInstanceApiUrl(config);
|
||||||
|
await requestWithGitHubGateway(config, gatewayService, gatewayV2Service, {
|
||||||
|
url: `https://${apiUrl}/user`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${credentials.personalAccessToken}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
personalAccessToken: credentials.personalAccessToken,
|
||||||
|
instanceType: credentials.instanceType,
|
||||||
|
host: credentials.host
|
||||||
|
};
|
||||||
|
} catch (e: unknown) {
|
||||||
|
logger.error(e, "Unable to verify GitHub PAT connection");
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate Personal Access Token: verify token has proper permissions"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||||
|
|||||||
@@ -38,6 +38,19 @@ export const GitHubConnectionAppInputCredentialsSchema = z.union([
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const GitHubConnectionPatInputCredentialsSchema = z.union([
|
||||||
|
z.object({
|
||||||
|
personalAccessToken: z.string().trim().min(1, "Personal Access Token required"),
|
||||||
|
instanceType: z.literal("server"),
|
||||||
|
host: z.string().trim().min(1, "Host is required for server instance type")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
personalAccessToken: z.string().trim().min(1, "Personal Access Token required"),
|
||||||
|
instanceType: z.literal("cloud").optional(),
|
||||||
|
host: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([
|
export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([
|
||||||
z.object({
|
z.object({
|
||||||
accessToken: z.string(),
|
accessToken: z.string(),
|
||||||
@@ -64,6 +77,19 @@ export const GitHubConnectionAppOutputCredentialsSchema = z.union([
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const GitHubConnectionPatOutputCredentialsSchema = z.union([
|
||||||
|
z.object({
|
||||||
|
personalAccessToken: z.string(),
|
||||||
|
instanceType: z.literal("server"),
|
||||||
|
host: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
personalAccessToken: z.string(),
|
||||||
|
instanceType: z.literal("cloud").optional(),
|
||||||
|
host: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
z.object({
|
z.object({
|
||||||
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||||
@@ -76,6 +102,12 @@ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("m
|
|||||||
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
||||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.Pat).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||||
|
credentials: GitHubConnectionPatInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||||
|
)
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -88,7 +120,11 @@ export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsS
|
|||||||
export const UpdateGitHubConnectionSchema = z
|
export const UpdateGitHubConnectionSchema = z
|
||||||
.object({
|
.object({
|
||||||
credentials: z
|
credentials: z
|
||||||
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
|
.union([
|
||||||
|
GitHubConnectionAppInputCredentialsSchema,
|
||||||
|
GitHubConnectionOAuthInputCredentialsSchema,
|
||||||
|
GitHubConnectionPatInputCredentialsSchema
|
||||||
|
])
|
||||||
.optional()
|
.optional()
|
||||||
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
||||||
})
|
})
|
||||||
@@ -110,6 +146,10 @@ export const GitHubConnectionSchema = z.intersection(
|
|||||||
z.object({
|
z.object({
|
||||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.Pat),
|
||||||
|
credentials: GitHubConnectionPatOutputCredentialsSchema
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -128,6 +168,13 @@ export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
|||||||
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
||||||
host: z.string().optional()
|
host: z.string().optional()
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
BaseGitHubConnectionSchema.extend({
|
||||||
|
method: z.literal(GitHubConnectionMethod.Pat),
|
||||||
|
credentials: z.object({
|
||||||
|
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
||||||
|
host: z.string().optional()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ import {
|
|||||||
THCVaultMountResponse
|
THCVaultMountResponse
|
||||||
} from "./hc-vault-connection-types";
|
} from "./hc-vault-connection-types";
|
||||||
|
|
||||||
|
// HashiCorp Vault stores JSON data, so values can be any valid JSON type
|
||||||
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||||
|
|
||||||
|
export const convertVaultValueToString = (value: JsonValue): string => {
|
||||||
|
if (value === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
// For objects and arrays, serialize as JSON
|
||||||
|
return JSON.stringify(value);
|
||||||
|
};
|
||||||
|
|
||||||
// Concurrency limit for HC Vault API requests to avoid rate limiting
|
// Concurrency limit for HC Vault API requests to avoid rate limiting
|
||||||
const HC_VAULT_CONCURRENCY_LIMIT = 20;
|
const HC_VAULT_CONCURRENCY_LIMIT = 20;
|
||||||
|
|
||||||
@@ -598,7 +615,7 @@ export const getHCVaultSecretsForPath = async (
|
|||||||
// For KV v2: /v1/{mount}/data/{path}
|
// For KV v2: /v1/{mount}/data/{path}
|
||||||
const { data } = await requestWithHCVaultGateway<{
|
const { data } = await requestWithHCVaultGateway<{
|
||||||
data: {
|
data: {
|
||||||
data: Record<string, string>; // KV v2 has nested data structure
|
data: Record<string, JsonValue>; // KV v2 has nested data structure, supports all JSON types
|
||||||
metadata: {
|
metadata: {
|
||||||
created_time: string;
|
created_time: string;
|
||||||
deletion_time: string;
|
deletion_time: string;
|
||||||
@@ -620,7 +637,7 @@ export const getHCVaultSecretsForPath = async (
|
|||||||
|
|
||||||
// For KV v1: /v1/{mount}/{path}
|
// For KV v1: /v1/{mount}/{path}
|
||||||
const { data } = await requestWithHCVaultGateway<{
|
const { data } = await requestWithHCVaultGateway<{
|
||||||
data: Record<string, string>; // KV v1 has flat data structure
|
data: Record<string, JsonValue>; // KV v1 has flat data structure, supports all JSON types
|
||||||
lease_duration: number;
|
lease_duration: number;
|
||||||
lease_id: string;
|
lease_id: string;
|
||||||
renewable: boolean;
|
renewable: boolean;
|
||||||
|
|||||||
+57
-26
@@ -2,12 +2,14 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionCertificateProfileActions,
|
||||||
ProjectPermissionPkiTemplateActions,
|
ProjectPermissionPkiTemplateActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
@@ -1180,7 +1182,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
extendedKeyUsages,
|
extendedKeyUsages,
|
||||||
signatureAlgorithm,
|
signatureAlgorithm,
|
||||||
keyAlgorithm,
|
keyAlgorithm,
|
||||||
isFromProfile
|
isFromProfile,
|
||||||
|
internal = false,
|
||||||
|
tx
|
||||||
}: TIssueCertFromCaDTO) => {
|
}: TIssueCertFromCaDTO) => {
|
||||||
let ca: TCertificateAuthorityWithAssociatedCa | undefined;
|
let ca: TCertificateAuthorityWithAssociatedCa | undefined;
|
||||||
let certificateTemplate: TCertificateTemplates | undefined;
|
let certificateTemplate: TCertificateTemplates | undefined;
|
||||||
@@ -1210,19 +1214,28 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
if (!internal) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actorId,
|
actor,
|
||||||
projectId: ca.projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId: ca.projectId,
|
||||||
actorOrgId,
|
actorAuthMethod,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actorOrgId,
|
||||||
});
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
if (isFromProfile) {
|
||||||
ProjectPermissionCertificateActions.Create,
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||||
);
|
ProjectPermissionSub.CertificateProfiles
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Create,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||||
if (!ca.internalCa.activeCaCertId)
|
if (!ca.internalCa.activeCaCertId)
|
||||||
@@ -1473,7 +1486,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
plainText: Buffer.from(certificateChainPem)
|
plainText: Buffer.from(certificateChainPem)
|
||||||
});
|
});
|
||||||
|
|
||||||
await certificateDAL.transaction(async (tx) => {
|
const executeIssueCertOperations = async (transaction: Knex) => {
|
||||||
const cert = await certificateDAL.create(
|
const cert = await certificateDAL.create(
|
||||||
{
|
{
|
||||||
caId: (ca as TCertificateAuthorities).id,
|
caId: (ca as TCertificateAuthorities).id,
|
||||||
@@ -1488,9 +1501,11 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
notAfter: notAfterDate,
|
notAfter: notAfterDate,
|
||||||
keyUsages: selectedKeyUsages,
|
keyUsages: selectedKeyUsages,
|
||||||
extendedKeyUsages: selectedExtendedKeyUsages,
|
extendedKeyUsages: selectedExtendedKeyUsages,
|
||||||
projectId: ca!.projectId
|
projectId: ca!.projectId,
|
||||||
|
keyAlgorithm: effectiveKeyAlgorithm,
|
||||||
|
signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm
|
||||||
},
|
},
|
||||||
tx
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
await certificateBodyDAL.create(
|
await certificateBodyDAL.create(
|
||||||
@@ -1499,7 +1514,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
encryptedCertificate,
|
encryptedCertificate,
|
||||||
encryptedCertificateChain
|
encryptedCertificateChain
|
||||||
},
|
},
|
||||||
tx
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
await certificateSecretDAL.create(
|
await certificateSecretDAL.create(
|
||||||
@@ -1507,7 +1522,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
encryptedPrivateKey
|
encryptedPrivateKey
|
||||||
},
|
},
|
||||||
tx
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (collectionId) {
|
if (collectionId) {
|
||||||
@@ -1516,12 +1531,18 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
pkiCollectionId: collectionId,
|
pkiCollectionId: collectionId,
|
||||||
certId: cert.id
|
certId: cert.id
|
||||||
},
|
},
|
||||||
tx
|
transaction
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cert;
|
return cert;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
await executeIssueCertOperations(tx);
|
||||||
|
} else {
|
||||||
|
await certificateDAL.transaction(executeIssueCertOperations);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: leafCert.toString("pem"),
|
certificate: leafCert.toString("pem"),
|
||||||
@@ -1593,10 +1614,17 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
if (dto.isFromProfile && dto.profileId) {
|
||||||
ProjectPermissionCertificateActions.Create,
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||||
);
|
ProjectPermissionSub.CertificateProfiles
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Create,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||||
@@ -1700,7 +1728,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
signatureAlgorithm: alg
|
||||||
});
|
});
|
||||||
|
|
||||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||||
@@ -1917,7 +1946,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
notAfter: notAfterDate,
|
notAfter: notAfterDate,
|
||||||
keyUsages: selectedKeyUsages,
|
keyUsages: selectedKeyUsages,
|
||||||
extendedKeyUsages: selectedExtendedKeyUsages,
|
extendedKeyUsages: selectedExtendedKeyUsages,
|
||||||
projectId: ca!.projectId
|
projectId: ca!.projectId,
|
||||||
|
keyAlgorithm: keyAlgorithm || ca!.internalCa!.keyAlgorithm,
|
||||||
|
signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
+6
@@ -1,3 +1,4 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
@@ -139,6 +140,9 @@ export type TIssueCertFromCaDTO = {
|
|||||||
signatureAlgorithm?: CertSignatureAlgorithm;
|
signatureAlgorithm?: CertSignatureAlgorithm;
|
||||||
keyAlgorithm?: CertKeyAlgorithm;
|
keyAlgorithm?: CertKeyAlgorithm;
|
||||||
isFromProfile?: boolean;
|
isFromProfile?: boolean;
|
||||||
|
profileId?: string;
|
||||||
|
internal?: boolean;
|
||||||
|
tx?: Knex;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TSignCertFromCaDTO =
|
export type TSignCertFromCaDTO =
|
||||||
@@ -159,6 +163,7 @@ export type TSignCertFromCaDTO =
|
|||||||
signatureAlgorithm?: string;
|
signatureAlgorithm?: string;
|
||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
isFromProfile?: boolean;
|
isFromProfile?: boolean;
|
||||||
|
profileId?: string;
|
||||||
}
|
}
|
||||||
| ({
|
| ({
|
||||||
isInternal: false;
|
isInternal: false;
|
||||||
@@ -177,6 +182,7 @@ export type TSignCertFromCaDTO =
|
|||||||
signatureAlgorithm?: string;
|
signatureAlgorithm?: string;
|
||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
isFromProfile?: boolean;
|
isFromProfile?: boolean;
|
||||||
|
profileId?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">);
|
} & Omit<TProjectPermission, "projectId">);
|
||||||
|
|
||||||
export type TGetCaCertificateTemplatesDTO = {
|
export type TGetCaCertificateTemplatesDTO = {
|
||||||
|
|||||||
@@ -175,6 +175,26 @@ export enum CertSignatureAlgorithm {
|
|||||||
ECDSA_SHA512 = "ECDSA-SHA512"
|
ECDSA_SHA512 = "ECDSA-SHA512"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CertificateRenewalErrorType {
|
||||||
|
TEMPLATE_VALIDATION_FAILED = "TEMPLATE_VALIDATION_FAILED",
|
||||||
|
CA_NOT_FOUND = "CA_NOT_FOUND",
|
||||||
|
CA_INACTIVE = "CA_INACTIVE",
|
||||||
|
CERTIFICATE_OUTLIVES_CA = "CERTIFICATE_OUTLIVES_CA",
|
||||||
|
TTL_TOO_SHORT = "TTL_TOO_SHORT",
|
||||||
|
NOT_ELIGIBLE = "NOT_ELIGIBLE",
|
||||||
|
VALIDITY_EXCEEDS_MAXIMUM = "VALIDITY_EXCEEDS_MAXIMUM",
|
||||||
|
NOT_ALLOWED_BY_TEMPLATE = "NOT_ALLOWED_BY_TEMPLATE",
|
||||||
|
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CERTIFICATE_RENEWAL_CONFIG = {
|
||||||
|
MIN_RENEW_BEFORE_DAYS: 1,
|
||||||
|
MAX_RENEW_BEFORE_DAYS: 30,
|
||||||
|
QUEUE_BATCH_SIZE: 100,
|
||||||
|
DAILY_CRON_SCHEDULE: "0 0 * * *",
|
||||||
|
QUEUE_START_DELAY_MS: 5000
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
|
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
|
||||||
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
|
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
|
||||||
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
|
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsageOIDToName,
|
||||||
|
CertKeyAlgorithm,
|
||||||
|
CertKeyUsage,
|
||||||
|
CertSignatureAlgorithm,
|
||||||
|
mapLegacyAltNameType,
|
||||||
|
TAltNameMapping,
|
||||||
|
TAltNameType
|
||||||
|
} from "../certificate/certificate-types";
|
||||||
|
import { parseDistinguishedName } from "../certificate-authority/certificate-authority-fns";
|
||||||
|
import { validateAndMapAltNameType } from "../certificate-authority/certificate-authority-validators";
|
||||||
|
import { TCertificateRequest } from "../certificate-template-v2/certificate-template-v2-types";
|
||||||
|
import { mapLegacyExtendedKeyUsageToStandard, mapLegacyKeyUsageToStandard } from "./certificate-constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts certificate request data from a CSR string
|
||||||
|
* @param csr - The CSR in PEM format
|
||||||
|
* @returns TCertificateRequest object with parsed CSR data
|
||||||
|
*/
|
||||||
|
export const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
const subject = parseDistinguishedName(csrObj.subject);
|
||||||
|
|
||||||
|
const certificateRequest: TCertificateRequest = {
|
||||||
|
commonName: subject.commonName,
|
||||||
|
organization: subject.organization,
|
||||||
|
organizationUnit: subject.ou,
|
||||||
|
locality: subject.locality,
|
||||||
|
state: subject.province,
|
||||||
|
country: subject.country
|
||||||
|
};
|
||||||
|
|
||||||
|
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||||
|
if (csrKeyUsageExtension) {
|
||||||
|
const csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||||
|
);
|
||||||
|
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||||
|
if (csrExtendedKeyUsageExtension) {
|
||||||
|
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||||
|
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||||
|
);
|
||||||
|
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||||
|
if (sanExtension) {
|
||||||
|
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||||
|
const altNamesArray: TAltNameMapping[] = sanNames.items
|
||||||
|
.filter(
|
||||||
|
(value) =>
|
||||||
|
value.type === TAltNameType.EMAIL ||
|
||||||
|
value.type === TAltNameType.DNS ||
|
||||||
|
value.type === TAltNameType.IP ||
|
||||||
|
value.type === TAltNameType.URL
|
||||||
|
)
|
||||||
|
.map((name): TAltNameMapping => {
|
||||||
|
const altNameType = validateAndMapAltNameType(name.value);
|
||||||
|
if (!altNameType) {
|
||||||
|
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
|
||||||
|
}
|
||||||
|
return altNameType;
|
||||||
|
});
|
||||||
|
|
||||||
|
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
|
||||||
|
type: mapLegacyAltNameType(altName.type),
|
||||||
|
value: altName.value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the key algorithm and signature algorithm from a CSR
|
||||||
|
* @param csr - The CSR in PEM format
|
||||||
|
* @returns Object containing keyAlgorithm and signatureAlgorithm
|
||||||
|
*/
|
||||||
|
export const extractAlgorithmsFromCSR = (csr: string) => {
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
|
||||||
|
// Extract key algorithm from public key
|
||||||
|
const { publicKey } = csrObj;
|
||||||
|
let keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
|
||||||
|
if (publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
||||||
|
const rsaPublicKey = publicKey as unknown as { algorithm: { modulusLength: number } };
|
||||||
|
const keySize = rsaPublicKey.algorithm.modulusLength;
|
||||||
|
switch (keySize) {
|
||||||
|
case 2048:
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.RSA_2048;
|
||||||
|
break;
|
||||||
|
case 3072:
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.RSA_3072;
|
||||||
|
break;
|
||||||
|
case 4096:
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.RSA_4096;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported RSA key size in CSR: ${keySize}. Supported: 2048, 3072, 4096`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (publicKey.algorithm.name === "ECDSA") {
|
||||||
|
const ecPublicKey = publicKey as unknown as { algorithm: { namedCurve: string } };
|
||||||
|
const { namedCurve } = ecPublicKey.algorithm;
|
||||||
|
switch (namedCurve) {
|
||||||
|
case "P-256":
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.ECDSA_P256;
|
||||||
|
break;
|
||||||
|
case "P-384":
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.ECDSA_P384;
|
||||||
|
break;
|
||||||
|
case "P-521":
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.ECDSA_P521;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported ECDSA curve in CSR: ${namedCurve}. Supported: P-256, P-384, P-521`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported key algorithm in CSR: ${publicKey.algorithm.name}. Supported: RSASSA-PKCS1-v1_5, ECDSA`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatureAlgorithm = csrObj.signatureAlgorithm.name;
|
||||||
|
const hashName = (csrObj.signatureAlgorithm as unknown as { hash?: { name: string } }).hash?.name;
|
||||||
|
|
||||||
|
let normalizedSignatureAlg: CertSignatureAlgorithm;
|
||||||
|
|
||||||
|
if (signatureAlgorithm === "RSASSA-PKCS1-v1_5") {
|
||||||
|
switch (hashName) {
|
||||||
|
case "SHA-256":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.RSA_SHA256;
|
||||||
|
break;
|
||||||
|
case "SHA-384":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.RSA_SHA384;
|
||||||
|
break;
|
||||||
|
case "SHA-512":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.RSA_SHA512;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported RSA hash algorithm in CSR: ${hashName}. Supported: SHA-256, SHA-384, SHA-512`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (signatureAlgorithm === "ECDSA") {
|
||||||
|
switch (hashName) {
|
||||||
|
case "SHA-256":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.ECDSA_SHA256;
|
||||||
|
break;
|
||||||
|
case "SHA-384":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.ECDSA_SHA384;
|
||||||
|
break;
|
||||||
|
case "SHA-512":
|
||||||
|
normalizedSignatureAlg = CertSignatureAlgorithm.ECDSA_SHA512;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported ECDSA hash algorithm in CSR: ${hashName}. Supported: SHA-256, SHA-384, SHA-512`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported signature algorithm in CSR: ${signatureAlgorithm}. Supported: RSASSA-PKCS1-v1_5, ECDSA`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyAlgorithm,
|
||||||
|
signatureAlgorithm: normalizedSignatureAlg
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -3,31 +3,15 @@ import * as x509 from "@peculiar/x509";
|
|||||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||||
import {
|
|
||||||
CertExtendedKeyUsageOIDToName,
|
|
||||||
CertKeyUsage,
|
|
||||||
mapLegacyAltNameType,
|
|
||||||
TAltNameMapping,
|
|
||||||
TAltNameType
|
|
||||||
} from "@app/services/certificate/certificate-types";
|
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import {
|
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
getCaCertChain,
|
|
||||||
getCaCertChains,
|
|
||||||
parseDistinguishedName
|
|
||||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
|
||||||
import { validateAndMapAltNameType } from "@app/services/certificate-authority/certificate-authority-validators";
|
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import {
|
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||||
mapLegacyExtendedKeyUsageToStandard,
|
|
||||||
mapLegacyKeyUsageToStandard
|
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
import { TCertificateRequest } from "@app/services/certificate-template-v2/certificate-template-v2-types";
|
|
||||||
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -61,63 +45,6 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
estEnrollmentConfigDAL
|
estEnrollmentConfigDAL
|
||||||
}: TCertificateEstV3ServiceFactoryDep) => {
|
}: TCertificateEstV3ServiceFactoryDep) => {
|
||||||
const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
|
|
||||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
|
||||||
const subject = parseDistinguishedName(csrObj.subject);
|
|
||||||
|
|
||||||
const certificateRequest: TCertificateRequest = {
|
|
||||||
commonName: subject.commonName,
|
|
||||||
organization: subject.organization,
|
|
||||||
organizationUnit: subject.ou,
|
|
||||||
locality: subject.locality,
|
|
||||||
state: subject.province,
|
|
||||||
country: subject.country
|
|
||||||
};
|
|
||||||
|
|
||||||
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
|
||||||
if (csrKeyUsageExtension) {
|
|
||||||
const csrKeyUsages = Object.values(CertKeyUsage).filter(
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
|
||||||
);
|
|
||||||
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
|
|
||||||
}
|
|
||||||
|
|
||||||
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
|
||||||
if (csrExtendedKeyUsageExtension) {
|
|
||||||
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
|
||||||
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
|
||||||
);
|
|
||||||
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
|
||||||
if (sanExtension) {
|
|
||||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
|
||||||
const altNamesArray: TAltNameMapping[] = sanNames.items
|
|
||||||
.filter(
|
|
||||||
(value) =>
|
|
||||||
value.type === TAltNameType.EMAIL ||
|
|
||||||
value.type === TAltNameType.DNS ||
|
|
||||||
value.type === TAltNameType.IP ||
|
|
||||||
value.type === TAltNameType.URL
|
|
||||||
)
|
|
||||||
.map((name): TAltNameMapping => {
|
|
||||||
const altNameType = validateAndMapAltNameType(name.value);
|
|
||||||
if (!altNameType) {
|
|
||||||
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
|
|
||||||
}
|
|
||||||
return altNameType;
|
|
||||||
});
|
|
||||||
|
|
||||||
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
|
|
||||||
type: mapLegacyAltNameType(altName.type),
|
|
||||||
value: altName.value
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificateRequest;
|
|
||||||
};
|
|
||||||
const simpleEnrollByProfile = async ({
|
const simpleEnrollByProfile = async ({
|
||||||
csr,
|
csr,
|
||||||
profileId,
|
profileId,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigEncryptedCaChain"),
|
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigEncryptedCaChain"),
|
||||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"),
|
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"),
|
||||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
|
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
|
||||||
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays")
|
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigRenewBeforeDays")
|
||||||
)
|
)
|
||||||
.where(`${TableName.PkiCertificateProfile}.id`, id)
|
.where(`${TableName.PkiCertificateProfile}.id`, id)
|
||||||
.first();
|
.first();
|
||||||
@@ -132,7 +132,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
? ({
|
? ({
|
||||||
id: result.apiConfigId,
|
id: result.apiConfigId,
|
||||||
autoRenew: !!result.apiConfigAutoRenew,
|
autoRenew: !!result.apiConfigAutoRenew,
|
||||||
autoRenewDays: result.apiConfigAutoRenewDays || undefined
|
renewBeforeDays: result.apiConfigRenewBeforeDays || undefined
|
||||||
} as TCertificateProfileWithConfigs["apiConfig"])
|
} as TCertificateProfileWithConfigs["apiConfig"])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
||||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
||||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
||||||
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays")
|
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (includeMetrics) {
|
if (includeMetrics) {
|
||||||
@@ -290,7 +290,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
||||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
||||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
||||||
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays"),
|
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
|
||||||
db.raw("COUNT(certificates.id) as total_certificates"),
|
db.raw("COUNT(certificates.id) as total_certificates"),
|
||||||
db.raw(
|
db.raw(
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
||||||
@@ -333,7 +333,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
? {
|
? {
|
||||||
id: result.apiId as string,
|
id: result.apiId as string,
|
||||||
autoRenew: !!result.apiAutoRenew,
|
autoRenew: !!result.apiAutoRenew,
|
||||||
autoRenewDays: (result.apiAutoRenewDays as number) || undefined
|
renewBeforeDays: (result.apiRenewBeforeDays as number) || undefined
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const createCertificateProfileSchema = z
|
|||||||
apiConfig: z
|
apiConfig: z
|
||||||
.object({
|
.object({
|
||||||
autoRenew: z.boolean().default(false),
|
autoRenew: z.boolean().default(false),
|
||||||
autoRenewDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
@@ -75,7 +75,7 @@ export const updateCertificateProfileSchema = z
|
|||||||
apiConfig: z
|
apiConfig: z
|
||||||
.object({
|
.object({
|
||||||
autoRenew: z.boolean().default(false),
|
autoRenew: z.boolean().default(false),
|
||||||
autoRenewDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ describe("CertificateProfileService", () => {
|
|||||||
apiConfig: {
|
apiConfig: {
|
||||||
id: "api-config-123",
|
id: "api-config-123",
|
||||||
autoRenew: true,
|
autoRenew: true,
|
||||||
autoRenewDays: 30
|
renewBeforeDays: 30
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
autoRenew: true,
|
autoRenew: true,
|
||||||
autoRenewDays: 30
|
renewBeforeDays: 30
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
autoRenew: true,
|
autoRenew: true,
|
||||||
autoRenewDays: 30
|
renewBeforeDays: 30
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -761,7 +761,7 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
autoRenew: true,
|
autoRenew: true,
|
||||||
autoRenewDays: 30
|
renewBeforeDays: 30
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -786,7 +786,7 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
autoRenew: true,
|
autoRenew: true,
|
||||||
autoRenewDays: 7
|
renewBeforeDays: 7
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -808,7 +808,7 @@ describe("CertificateProfileService", () => {
|
|||||||
expect(mockApiEnrollmentConfigDAL.create).toHaveBeenCalledWith(
|
expect(mockApiEnrollmentConfigDAL.create).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
autoRenew: true,
|
autoRenew: true,
|
||||||
autoRenewDays: 7
|
renewBeforeDays: 7
|
||||||
},
|
},
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
const apiConfig = await apiEnrollmentConfigDAL.create(
|
const apiConfig = await apiEnrollmentConfigDAL.create(
|
||||||
{
|
{
|
||||||
autoRenew: data.apiConfig.autoRenew,
|
autoRenew: data.apiConfig.autoRenew,
|
||||||
autoRenewDays: data.apiConfig.autoRenewDays
|
renewBeforeDays: data.apiConfig.renewBeforeDays
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -343,7 +343,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
existingProfile.apiConfigId,
|
existingProfile.apiConfigId,
|
||||||
{
|
{
|
||||||
autoRenew: apiConfig.autoRenew,
|
autoRenew: apiConfig.autoRenew,
|
||||||
autoRenewDays: apiConfig.autoRenewDays
|
renewBeforeDays: apiConfig.renewBeforeDays
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enr
|
|||||||
};
|
};
|
||||||
apiConfig?: {
|
apiConfig?: {
|
||||||
autoRenew?: boolean;
|
autoRenew?: boolean;
|
||||||
autoRenewDays?: number;
|
renewBeforeDays?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
|||||||
apiConfig?: {
|
apiConfig?: {
|
||||||
id: string;
|
id: string;
|
||||||
autoRenew: boolean;
|
autoRenew: boolean;
|
||||||
autoRenewDays?: number;
|
renewBeforeDays?: number;
|
||||||
};
|
};
|
||||||
metrics?: TCertificateProfileMetrics;
|
metrics?: TCertificateProfileMetrics;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -762,32 +762,36 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
templateId
|
templateId,
|
||||||
|
internal = false
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
templateId: string;
|
templateId: string;
|
||||||
|
internal?: boolean;
|
||||||
}): Promise<TCertificateTemplateV2> => {
|
}): Promise<TCertificateTemplateV2> => {
|
||||||
const template = await certificateTemplateV2DAL.findById(templateId);
|
const template = await certificateTemplateV2DAL.findById(templateId);
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Certificate template not found" });
|
throw new NotFoundError({ message: "Certificate template not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
if (!internal) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actorId,
|
actor,
|
||||||
projectId: template.projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId: template.projectId,
|
||||||
actorOrgId,
|
actorAuthMethod,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actorOrgId,
|
||||||
});
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionPkiTemplateActions.Read,
|
ProjectPermissionPkiTemplateActions.Read,
|
||||||
ProjectPermissionSub.CertificateTemplates
|
ProjectPermissionSub.CertificateTemplates
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return template;
|
return template;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
|
import { CERTIFICATE_RENEWAL_CONFIG } from "../certificate-common/certificate-constants";
|
||||||
|
import { TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
||||||
|
|
||||||
|
type TCertificateV3QueueServiceFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findCertificatesEligibleForRenewal" | "updateById">;
|
||||||
|
certificateV3Service: TCertificateV3ServiceFactory;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const certificateV3QueueServiceFactory = ({
|
||||||
|
queueService,
|
||||||
|
certificateDAL,
|
||||||
|
certificateV3Service,
|
||||||
|
auditLogService
|
||||||
|
}: TCertificateV3QueueServiceFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
if (appCfg.isSecondaryInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await queueService.stopRepeatableJob(
|
||||||
|
QueueName.CertificateV3AutoRenewal,
|
||||||
|
QueueJobs.CertificateV3DailyAutoRenewal,
|
||||||
|
{ pattern: CERTIFICATE_RENEWAL_CONFIG.DAILY_CRON_SCHEDULE, utc: true },
|
||||||
|
QueueName.CertificateV3AutoRenewal
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.startPg<QueueName.CertificateV3AutoRenewal>(
|
||||||
|
QueueJobs.CertificateV3DailyAutoRenewal,
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
logger.info(`${QueueJobs.CertificateV3DailyAutoRenewal}: queue task started`);
|
||||||
|
|
||||||
|
const { QUEUE_BATCH_SIZE } = CERTIFICATE_RENEWAL_CONFIG;
|
||||||
|
let offset = 0;
|
||||||
|
let hasMore = true;
|
||||||
|
let totalCertificatesFound = 0;
|
||||||
|
let totalCertificatesRenewed = 0;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const certificates = await certificateDAL.findCertificatesEligibleForRenewal({
|
||||||
|
limit: QUEUE_BATCH_SIZE,
|
||||||
|
offset
|
||||||
|
});
|
||||||
|
|
||||||
|
if (certificates.length === 0) {
|
||||||
|
hasMore = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCertificatesFound += certificates.length;
|
||||||
|
logger.info(
|
||||||
|
`${QueueJobs.CertificateV3DailyAutoRenewal}: found ${certificates.length} certificates eligible for renewal (batch ${Math.floor(offset / QUEUE_BATCH_SIZE) + 1}, total found so far: ${totalCertificatesFound})`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const certificate of certificates) {
|
||||||
|
try {
|
||||||
|
if (certificate.renewBeforeDays) {
|
||||||
|
const { MIN_RENEW_BEFORE_DAYS, MAX_RENEW_BEFORE_DAYS } = CERTIFICATE_RENEWAL_CONFIG;
|
||||||
|
if (
|
||||||
|
certificate.renewBeforeDays < MIN_RENEW_BEFORE_DAYS ||
|
||||||
|
certificate.renewBeforeDays > MAX_RENEW_BEFORE_DAYS
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateV3Service.renewCertificate({
|
||||||
|
actor: ActorType.PLATFORM,
|
||||||
|
actorId: "",
|
||||||
|
actorAuthMethod: null,
|
||||||
|
actorOrgId: "",
|
||||||
|
certificateId: certificate.id,
|
||||||
|
internal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
totalCertificatesRenewed += 1;
|
||||||
|
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.AUTOMATED_RENEW_CERTIFICATE,
|
||||||
|
metadata: {
|
||||||
|
certificateId: certificate.id,
|
||||||
|
commonName: certificate.commonName || "",
|
||||||
|
profileId: certificate.profileId!,
|
||||||
|
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
|
||||||
|
profileName: certificate.profileName || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error(error, `Failed to renew certificate ${certificate.id}: ${errorMessage}`);
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED,
|
||||||
|
metadata: {
|
||||||
|
certificateId: certificate.id,
|
||||||
|
commonName: certificate.commonName || "",
|
||||||
|
profileId: certificate.profileId || "",
|
||||||
|
renewBeforeDays: certificate.renewBeforeDays?.toString() || "",
|
||||||
|
profileName: certificate.profileName || "",
|
||||||
|
error: errorMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += QUEUE_BATCH_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`${QueueJobs.CertificateV3DailyAutoRenewal}: queue task completed. Renewed ${totalCertificatesRenewed} certificates out of ${totalCertificatesFound}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `${QueueJobs.CertificateV3DailyAutoRenewal}: certificate renewal failed`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
batchSize: 1,
|
||||||
|
workerCount: 1,
|
||||||
|
pollingIntervalSeconds: 60
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queueService.schedulePg(
|
||||||
|
QueueJobs.CertificateV3DailyAutoRenewal,
|
||||||
|
CERTIFICATE_RENEWAL_CONFIG.DAILY_CRON_SCHEDULE,
|
||||||
|
undefined,
|
||||||
|
{ tz: "UTC" }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
init
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateV3QueueServiceFactory = ReturnType<typeof certificateV3QueueServiceFactory>;
|
||||||
@@ -7,10 +7,12 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { ACMESANType, CertificateOrderStatus } from "@app/services/certificate/certificate-types";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
|
import { ACMESANType, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsageType,
|
CertExtendedKeyUsageType,
|
||||||
@@ -23,14 +25,32 @@ import { EnrollmentType } from "@app/services/certificate-profile/certificate-pr
|
|||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
|
||||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||||
|
import {
|
||||||
|
extractAlgorithmsFromCSR,
|
||||||
|
extractCertificateRequestFromCSR
|
||||||
|
} from "../certificate-common/certificate-csr-utils";
|
||||||
import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
import { certificateV3ServiceFactory, TCertificateV3ServiceFactory } from "./certificate-v3-service";
|
||||||
|
|
||||||
|
vi.mock("../certificate-common/certificate-csr-utils", () => ({
|
||||||
|
extractCertificateRequestFromCSR: vi.fn(),
|
||||||
|
extractAlgorithmsFromCSR: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
describe("CertificateV3Service", () => {
|
describe("CertificateV3Service", () => {
|
||||||
let service: TCertificateV3ServiceFactory;
|
let service: TCertificateV3ServiceFactory;
|
||||||
|
|
||||||
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "updateById"> = {
|
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
|
||||||
findOne: vi.fn(),
|
findOne: vi.fn(),
|
||||||
updateById: vi.fn()
|
findById: vi.fn(),
|
||||||
|
updateById: vi.fn(),
|
||||||
|
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne"> = {
|
||||||
|
findOne: vi.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
||||||
@@ -76,7 +96,7 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset all mocks before each test
|
// Reset all mocks before each test
|
||||||
vi.clearAllMocks();
|
vi.resetAllMocks();
|
||||||
|
|
||||||
// Mock ForbiddenError.from static method
|
// Mock ForbiddenError.from static method
|
||||||
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
||||||
@@ -95,8 +115,20 @@ describe("CertificateV3Service", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mocked(extractCertificateRequestFromCSR).mockReturnValue({
|
||||||
|
commonName: "test.example.com",
|
||||||
|
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||||
|
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH]
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(extractAlgorithmsFromCSR).mockReturnValue({
|
||||||
|
keyAlgorithm: "RSA_2048" as any,
|
||||||
|
signatureAlgorithm: "RSA-SHA256" as any
|
||||||
|
});
|
||||||
|
|
||||||
service = certificateV3ServiceFactory({
|
service = certificateV3ServiceFactory({
|
||||||
certificateDAL: mockCertificateDAL,
|
certificateDAL: mockCertificateDAL,
|
||||||
|
certificateSecretDAL: mockCertificateSecretDAL,
|
||||||
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
||||||
certificateProfileDAL: mockCertificateProfileDAL,
|
certificateProfileDAL: mockCertificateProfileDAL,
|
||||||
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||||
@@ -641,6 +673,11 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
|
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||||
@@ -1460,4 +1497,714 @@ describe("CertificateV3Service", () => {
|
|||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("renewCertificate", () => {
|
||||||
|
const mockOriginalCert = {
|
||||||
|
id: "cert-123",
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
serialNumber: "123456",
|
||||||
|
friendlyName: "Test Certificate",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
notBefore: new Date("2024-01-01"),
|
||||||
|
notAfter: new Date("2024-02-01"), // 31 days
|
||||||
|
revokedAt: null,
|
||||||
|
renewedByCertificateId: null,
|
||||||
|
profileId: "profile-123",
|
||||||
|
renewBeforeDays: 7,
|
||||||
|
caId: "ca-123",
|
||||||
|
pkiSubscriberId: null,
|
||||||
|
keyUsages: ["digital_signature", "key_agreement"],
|
||||||
|
extendedKeyUsages: ["server_auth"],
|
||||||
|
altNames: "test.example.com,api.example.com",
|
||||||
|
projectId: "project-123",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
certificateTemplateId: "template-123",
|
||||||
|
revocationReason: null,
|
||||||
|
caCertId: null,
|
||||||
|
renewedFromCertificateId: null,
|
||||||
|
renewalError: null,
|
||||||
|
keyAlgorithm: "RSA_2048",
|
||||||
|
signatureAlgorithm: "RSA-SHA256"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProfile = {
|
||||||
|
id: "profile-123",
|
||||||
|
projectId: "project-123",
|
||||||
|
enrollmentType: EnrollmentType.API,
|
||||||
|
caId: "ca-123",
|
||||||
|
certificateTemplateId: "template-123",
|
||||||
|
apiConfig: {
|
||||||
|
id: "api-config-123",
|
||||||
|
autoRenew: true,
|
||||||
|
renewBeforeDays: 14
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
slug: "test-profile",
|
||||||
|
description: "Test profile"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCA = {
|
||||||
|
id: "ca-123",
|
||||||
|
projectId: "project-123",
|
||||||
|
status: CaStatus.ACTIVE,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
enableDirectIssuance: true,
|
||||||
|
name: "Test CA",
|
||||||
|
requireTemplateForIssuance: false,
|
||||||
|
externalCa: undefined,
|
||||||
|
parentCaId: null,
|
||||||
|
type: "ROOT",
|
||||||
|
friendlyName: "Test CA",
|
||||||
|
organization: "Test Org",
|
||||||
|
ou: "Test OU",
|
||||||
|
country: "US",
|
||||||
|
province: "CA",
|
||||||
|
locality: "SF",
|
||||||
|
commonName: "Test CA",
|
||||||
|
keyAlgorithm: "RSA_2048",
|
||||||
|
notAfter: "2025-01-01T00:00:00Z",
|
||||||
|
notBefore: "2024-01-01T00:00:00Z",
|
||||||
|
maxPathLength: -1,
|
||||||
|
activeCaCertId: "cert-123",
|
||||||
|
dn: "CN=Test CA,O=Test Org,OU=Test OU,C=US",
|
||||||
|
serialNumber: "123456789",
|
||||||
|
internalCa: {
|
||||||
|
id: "internal-ca-123",
|
||||||
|
parentCaId: null,
|
||||||
|
type: "ROOT",
|
||||||
|
friendlyName: "Test CA",
|
||||||
|
organization: "Test Org",
|
||||||
|
ou: "Test OU",
|
||||||
|
country: "US",
|
||||||
|
province: "CA",
|
||||||
|
locality: "SF",
|
||||||
|
commonName: "Test CA",
|
||||||
|
keyAlgorithm: "RSA_2048",
|
||||||
|
notAfter: "2025-01-01T00:00:00Z",
|
||||||
|
notBefore: "2024-01-01T00:00:00Z",
|
||||||
|
maxPathLength: -1,
|
||||||
|
activeCaCertId: "cert-123",
|
||||||
|
dn: "CN=Test CA,O=Test Org,OU=Test OU,C=US",
|
||||||
|
serialNumber: "123456789"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTemplate = {
|
||||||
|
id: "template-123",
|
||||||
|
projectId: "project-123",
|
||||||
|
name: "Test Template",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
algorithms: {
|
||||||
|
signature: ["SHA256-RSA", "SHA384-RSA"],
|
||||||
|
keyType: ["RSA_2048", "RSA_4096"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock current date to be within renewal window
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2024-01-26")); // 6 days before cert expires, within renewal window
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully renew eligible certificate", async () => {
|
||||||
|
// Mock the initial findById call
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({
|
||||||
|
certificate: "renewed-cert",
|
||||||
|
certificateChain: "renewed-chain",
|
||||||
|
issuingCaCertificate: "issuing-ca",
|
||||||
|
privateKey: "private-key",
|
||||||
|
serialNumber: "789012",
|
||||||
|
ca: mockCA
|
||||||
|
});
|
||||||
|
|
||||||
|
const newCert = { ...mockOriginalCert, id: "cert-456", serialNumber: "789012" };
|
||||||
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
|
||||||
|
|
||||||
|
// Mock the transaction to return the expected structure
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
const result = await callback(mockTx);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveProperty("certificate", "renewed-cert");
|
||||||
|
expect(result).toHaveProperty("certificateId", "cert-456");
|
||||||
|
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
|
||||||
|
"cert-456",
|
||||||
|
{
|
||||||
|
profileId: "profile-123",
|
||||||
|
renewBeforeDays: 14,
|
||||||
|
renewedFromCertificateId: "cert-123"
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith(
|
||||||
|
"cert-123",
|
||||||
|
{
|
||||||
|
renewedByCertificateId: "cert-456",
|
||||||
|
renewalError: null
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should validate certificate against current template during renewal", async () => {
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
||||||
|
isValid: false,
|
||||||
|
errors: ["Subject alternative name not allowed"],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
// Mock updateById to handle the renewal error logging
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle errors
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate renewal failed. Errors: Subject alternative name not allowed");
|
||||||
|
|
||||||
|
// Should store template validation error
|
||||||
|
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
|
||||||
|
renewalError: "Template validation failed: Subject alternative name not allowed"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if certificate is not from a profile", async () => {
|
||||||
|
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle errors
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(ForbiddenRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Only certificates issued from a profile can be renewed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if certificate was issued from CSR (external private key)", async () => {
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue(null as any);
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(ForbiddenRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("certificates issued from CSR (external private key) cannot be renewed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if certificate is already renewed", async () => {
|
||||||
|
const alreadyRenewedCert = { ...mockOriginalCert, renewedByCertificateId: "cert-456" };
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(alreadyRenewedCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
// Mock updateById to handle the renewal error logging
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(alreadyRenewedCert);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle errors
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate has already been renewed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if certificate is expired", async () => {
|
||||||
|
const expiredCert = {
|
||||||
|
...mockOriginalCert,
|
||||||
|
notAfter: new Date("2024-01-20") // Expired 6 days ago
|
||||||
|
};
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(expiredCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
// Mock updateById to handle the renewal error logging
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(expiredCert);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle errors
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate is already expired");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if certificate is revoked", async () => {
|
||||||
|
const revokedCert = {
|
||||||
|
...mockOriginalCert,
|
||||||
|
revokedAt: new Date("2024-01-15")
|
||||||
|
};
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(revokedCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
// Mock updateById to handle the renewal error logging
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(revokedCert);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle errors
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate is revoked and cannot be renewed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if CA is inactive", async () => {
|
||||||
|
const inactiveCA = { ...mockCA, status: CaStatus.DISABLED };
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(inactiveCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
// Mock updateById to handle the renewal error logging
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle errors
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate is not eligible for renewal: Certificate Authority is disabled, must be active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject renewal if new certificate would outlive CA", async () => {
|
||||||
|
const shortLivedCA = {
|
||||||
|
...mockCA,
|
||||||
|
internalCa: {
|
||||||
|
...mockCA.internalCa,
|
||||||
|
notAfter: "2024-01-28T00:00:00Z"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(shortLivedCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
// Mock updateById to handle the renewal error logging
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockOriginalCert);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle errors
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).rejects.toThrow(/New certificate would expire \(.+\) after its issuing CA \(.+\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow manual renewal outside window (manual renewal always bypasses window)", async () => {
|
||||||
|
vi.setSystemTime(new Date("2024-01-15")); // 17 days before expiry, outside 7-day window
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
|
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue({
|
||||||
|
certificate: "renewed-cert",
|
||||||
|
certificateChain: "renewed-chain",
|
||||||
|
issuingCaCertificate: "issuing-ca",
|
||||||
|
privateKey: "private-key",
|
||||||
|
serialNumber: "789012",
|
||||||
|
ca: mockCA
|
||||||
|
});
|
||||||
|
|
||||||
|
const newCert = { ...mockOriginalCert, id: "cert-456", serialNumber: "789012" };
|
||||||
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(newCert);
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(newCert);
|
||||||
|
|
||||||
|
// Set up transaction mock to properly handle the renewal process
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.renewCertificate({
|
||||||
|
certificateId: "cert-123",
|
||||||
|
...mockActor
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveProperty("certificate", "renewed-cert");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateRenewalConfig", () => {
|
||||||
|
it("should update renewal configuration successfully", async () => {
|
||||||
|
const mockCert = {
|
||||||
|
id: "cert-123",
|
||||||
|
profileId: "profile-123",
|
||||||
|
renewedByCertificateId: null,
|
||||||
|
notBefore: new Date("2026-01-01"),
|
||||||
|
notAfter: new Date("2026-02-01"),
|
||||||
|
projectId: "project-123",
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
revokedAt: null,
|
||||||
|
commonName: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProfile = {
|
||||||
|
id: "profile-123",
|
||||||
|
enrollmentType: EnrollmentType.API,
|
||||||
|
projectId: "project-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCert as any);
|
||||||
|
|
||||||
|
const result = await service.updateRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123",
|
||||||
|
renewBeforeDays: 7
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectId: "project-123",
|
||||||
|
renewBeforeDays: 7,
|
||||||
|
commonName: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
|
||||||
|
renewBeforeDays: 7
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject update if certificate is not from profile", async () => {
|
||||||
|
const mockCert = {
|
||||||
|
id: "cert-123",
|
||||||
|
profileId: null,
|
||||||
|
renewedByCertificateId: null,
|
||||||
|
projectId: "project-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123",
|
||||||
|
renewBeforeDays: 7
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123",
|
||||||
|
renewBeforeDays: 7
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate is not eligible for auto-renewal: certificate was not issued from a profile");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject update if certificate is already renewed", async () => {
|
||||||
|
const mockCert = {
|
||||||
|
id: "cert-123",
|
||||||
|
profileId: "profile-123",
|
||||||
|
renewedByCertificateId: "cert-456",
|
||||||
|
projectId: "project-123",
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
revokedAt: null,
|
||||||
|
notBefore: new Date("2026-01-01"),
|
||||||
|
notAfter: new Date("2026-02-01")
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProfile = {
|
||||||
|
id: "profile-123",
|
||||||
|
enrollmentType: EnrollmentType.API,
|
||||||
|
projectId: "project-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123",
|
||||||
|
renewBeforeDays: 7
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123",
|
||||||
|
renewBeforeDays: 7
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate is not eligible for auto-renewal: certificate has already been renewed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject update if renewBeforeDays >= certificate TTL", async () => {
|
||||||
|
const mockCert = {
|
||||||
|
id: "cert-123",
|
||||||
|
profileId: "profile-123",
|
||||||
|
renewedByCertificateId: null,
|
||||||
|
notBefore: new Date("2026-01-01"),
|
||||||
|
notAfter: new Date("2026-01-08"),
|
||||||
|
projectId: "project-123",
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
revokedAt: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProfile = {
|
||||||
|
id: "profile-123",
|
||||||
|
enrollmentType: EnrollmentType.API,
|
||||||
|
projectId: "project-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
||||||
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123",
|
||||||
|
renewBeforeDays: 8 // Greater than 7-day TTL
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123",
|
||||||
|
renewBeforeDays: 8
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Invalid renewal configuration: renewal threshold exceeds certificate validity period");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("disableRenewalConfig", () => {
|
||||||
|
it("should disable renewal configuration successfully", async () => {
|
||||||
|
const mockCert = {
|
||||||
|
id: "cert-123",
|
||||||
|
profileId: "profile-123",
|
||||||
|
projectId: "project-123",
|
||||||
|
commonName: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProfile = {
|
||||||
|
id: "profile-123",
|
||||||
|
enrollmentType: EnrollmentType.API,
|
||||||
|
projectId: "project-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile as any);
|
||||||
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCert as any);
|
||||||
|
|
||||||
|
const result = await service.disableRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectId: "project-123",
|
||||||
|
commonName: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateDAL.updateById).toHaveBeenCalledWith("cert-123", {
|
||||||
|
renewBeforeDays: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject disable if certificate is not from profile", async () => {
|
||||||
|
const mockCert = {
|
||||||
|
id: "cert-123",
|
||||||
|
profileId: null,
|
||||||
|
projectId: "project-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCert as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.disableRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123"
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.disableRenewalConfig({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "user-123",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "org-123",
|
||||||
|
certificateId: "cert-123"
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Certificate is not eligible for auto-renewal: certificate was not issued from a profile");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import {
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCertificateProfileActions,
|
ProjectPermissionCertificateProfileActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
|
CertExtendedKeyUsage,
|
||||||
CertificateOrderStatus,
|
CertificateOrderStatus,
|
||||||
CertKeyAlgorithm,
|
CertKeyAlgorithm,
|
||||||
CertSignatureAlgorithm
|
CertKeyType,
|
||||||
|
CertKeyUsage,
|
||||||
|
CertSignatureAlgorithm,
|
||||||
|
CertStatus
|
||||||
} from "@app/services/certificate/certificate-types";
|
} from "@app/services/certificate/certificate-types";
|
||||||
import {
|
import {
|
||||||
TCertificateAuthorityDALFactory,
|
TCertificateAuthorityDALFactory,
|
||||||
TCertificateAuthorityWithAssociatedCa
|
TCertificateAuthorityWithAssociatedCa
|
||||||
} from "@app/services/certificate-authority/certificate-authority-dal";
|
} from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
|
||||||
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
||||||
|
import {
|
||||||
|
extractAlgorithmsFromCSR,
|
||||||
|
extractCertificateRequestFromCSR
|
||||||
|
} from "../certificate-common/certificate-csr-utils";
|
||||||
import {
|
import {
|
||||||
bufferToString,
|
bufferToString,
|
||||||
buildCertificateSubjectFromTemplate,
|
buildCertificateSubjectFromTemplate,
|
||||||
buildSubjectAlternativeNamesFromTemplate,
|
buildSubjectAlternativeNamesFromTemplate,
|
||||||
|
convertExtendedKeyUsageArrayFromLegacy,
|
||||||
convertExtendedKeyUsageArrayToLegacy,
|
convertExtendedKeyUsageArrayToLegacy,
|
||||||
|
convertKeyUsageArrayFromLegacy,
|
||||||
convertKeyUsageArrayToLegacy,
|
convertKeyUsageArrayToLegacy,
|
||||||
mapEnumsForValidation,
|
mapEnumsForValidation,
|
||||||
normalizeDateForApi
|
normalizeDateForApi
|
||||||
@@ -38,13 +51,19 @@ import {
|
|||||||
import {
|
import {
|
||||||
TCertificateFromProfileResponse,
|
TCertificateFromProfileResponse,
|
||||||
TCertificateOrderResponse,
|
TCertificateOrderResponse,
|
||||||
|
TDisableRenewalConfigDTO,
|
||||||
|
TDisableRenewalResponse,
|
||||||
TIssueCertificateFromProfileDTO,
|
TIssueCertificateFromProfileDTO,
|
||||||
TOrderCertificateFromProfileDTO,
|
TOrderCertificateFromProfileDTO,
|
||||||
TSignCertificateFromProfileDTO
|
TRenewalConfigResponse,
|
||||||
|
TRenewCertificateDTO,
|
||||||
|
TSignCertificateFromProfileDTO,
|
||||||
|
TUpdateRenewalConfigDTO
|
||||||
} from "./certificate-v3-types";
|
} from "./certificate-v3-types";
|
||||||
|
|
||||||
type TCertificateV3ServiceFactoryDep = {
|
type TCertificateV3ServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "updateById">;
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
||||||
certificateTemplateV2Service: Pick<
|
certificateTemplateV2Service: Pick<
|
||||||
@@ -95,6 +114,77 @@ const validateProfileAndPermissions = async (
|
|||||||
return profile;
|
return profile;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateRenewalEligibility = (
|
||||||
|
certificate: {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
notBefore: Date;
|
||||||
|
notAfter: Date;
|
||||||
|
revokedAt?: Date | null;
|
||||||
|
renewedByCertificateId?: string | null;
|
||||||
|
profileId?: string | null;
|
||||||
|
caId?: string | null;
|
||||||
|
pkiSubscriberId?: string | null;
|
||||||
|
},
|
||||||
|
ca: TCertificateAuthorityWithAssociatedCa
|
||||||
|
) => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (certificate.status !== CertStatus.ACTIVE) {
|
||||||
|
errors.push(`Certificate status is ${certificate.status}, must be ${CertStatus.ACTIVE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
if (certificate.notAfter <= now) {
|
||||||
|
errors.push("Certificate is already expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificate.revokedAt) {
|
||||||
|
errors.push("Certificate is revoked and cannot be renewed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||||
|
const isInternalCa = caType === CaType.INTERNAL;
|
||||||
|
const isConnectedExternalCa = caType === CaType.ACME || caType === CaType.AZURE_AD_CS;
|
||||||
|
const isImportedCertificate = certificate.pkiSubscriberId != null && !certificate.profileId;
|
||||||
|
|
||||||
|
if (!isInternalCa && !isConnectedExternalCa) {
|
||||||
|
errors.push(`CA type ${String(caType)} does not support renewal`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isImportedCertificate) {
|
||||||
|
errors.push("Externally imported certificates cannot be renewed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ca.status !== CaStatus.ACTIVE) {
|
||||||
|
errors.push(`Certificate Authority is ${ca.status}, must be ${CaStatus.ACTIVE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificate.renewedByCertificateId) {
|
||||||
|
errors.push("Certificate has already been renewed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTtlInDays = Math.ceil(
|
||||||
|
(certificate.notAfter.getTime() - certificate.notBefore.getTime()) / (24 * 60 * 60 * 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ca.internalCa?.notAfter) {
|
||||||
|
const caExpiryDate = new Date(ca.internalCa.notAfter);
|
||||||
|
const proposedCertExpiryDate = new Date(now.getTime() + certificateTtlInDays * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
if (proposedCertExpiryDate > caExpiryDate) {
|
||||||
|
errors.push(
|
||||||
|
`New certificate would expire (${proposedCertExpiryDate.toISOString()}) after its issuing CA (${caExpiryDate.toISOString()})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isEligible: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation: string) => {
|
const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation: string) => {
|
||||||
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||||
if (caType !== CaType.INTERNAL) {
|
if (caType !== CaType.INTERNAL) {
|
||||||
@@ -129,11 +219,11 @@ const validateAlgorithmCompatibility = (
|
|||||||
const keyType = parts[parts.length - 1];
|
const keyType = parts[parts.length - 1];
|
||||||
|
|
||||||
if (caKeyAlgorithm.startsWith("RSA")) {
|
if (caKeyAlgorithm.startsWith("RSA")) {
|
||||||
return keyType === "RSA";
|
return keyType === CertKeyType.RSA;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caKeyAlgorithm.startsWith("EC")) {
|
if (caKeyAlgorithm.startsWith("EC")) {
|
||||||
return keyType === "ECDSA";
|
return keyType === CertKeyType.ECDSA;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -155,8 +245,85 @@ const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | s
|
|||||||
return bufferToString(certData as unknown as Buffer);
|
return bufferToString(certData as unknown as Buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseKeyUsages = (keyUsages: unknown): CertKeyUsage[] => {
|
||||||
|
if (!keyUsages) return [];
|
||||||
|
if (Array.isArray(keyUsages)) return keyUsages as CertKeyUsage[];
|
||||||
|
return (keyUsages as string).split(",").map((usage) => usage.trim() as CertKeyUsage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => {
|
||||||
|
if (!extendedKeyUsages) return [];
|
||||||
|
if (Array.isArray(extendedKeyUsages)) return extendedKeyUsages as CertExtendedKeyUsage[];
|
||||||
|
return (extendedKeyUsages as string).split(",").map((usage) => usage.trim() as CertExtendedKeyUsage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidRenewalTiming = (renewBeforeDays: number, certificateExpiryDate: Date): boolean => {
|
||||||
|
const renewalDate = new Date(certificateExpiryDate.getTime() - renewBeforeDays * 24 * 60 * 60 * 1000);
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
tomorrow.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return renewalDate >= tomorrow;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRenewalThreshold = (
|
||||||
|
profileRenewBeforeDays: number | undefined,
|
||||||
|
certificateTtlInDays: number
|
||||||
|
): number | undefined => {
|
||||||
|
if (!profileRenewBeforeDays) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificateTtlInDays > profileRenewBeforeDays) {
|
||||||
|
return profileRenewBeforeDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(1, certificateTtlInDays - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTtlToDays = (ttl: string): number => {
|
||||||
|
const match = ttl.match(new RE2("^(\\d+)([dhm])$"));
|
||||||
|
if (!match) {
|
||||||
|
throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, value, unit] = match;
|
||||||
|
const numValue = parseInt(value, 10);
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case "d":
|
||||||
|
return numValue;
|
||||||
|
case "h":
|
||||||
|
return Math.ceil(numValue / 24);
|
||||||
|
case "m":
|
||||||
|
return Math.ceil(numValue / (24 * 60));
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Unsupported TTL unit: ${unit}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateFinalRenewBeforeDays = (
|
||||||
|
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
|
||||||
|
ttl: string,
|
||||||
|
certificateExpiryDate: Date
|
||||||
|
): number | undefined => {
|
||||||
|
if (!profile.apiConfig?.autoRenew || !profile.apiConfig.renewBeforeDays) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTtlInDays = parseTtlToDays(ttl);
|
||||||
|
const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig.renewBeforeDays, certificateTtlInDays);
|
||||||
|
|
||||||
|
if (!renewBeforeDays) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const certificateV3ServiceFactory = ({
|
export const certificateV3ServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
@@ -198,7 +365,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
templateId: profile.certificateTemplateId
|
templateId: profile.certificateTemplateId,
|
||||||
|
internal: true
|
||||||
});
|
});
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
@@ -222,10 +390,6 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
validateCaSupport(ca, "direct certificate issuance");
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
|
|
||||||
if (!actorAuthMethod) {
|
|
||||||
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
|
|
||||||
}
|
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
||||||
@@ -274,7 +438,16 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, { profileId });
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||||
|
profile,
|
||||||
|
certificateRequest.validity.ttl,
|
||||||
|
new Date(cert.notAfter)
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateDAL.updateById(cert.id, {
|
||||||
|
profileId,
|
||||||
|
renewBeforeDays: finalRenewBeforeDays
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: bufferToString(certificate),
|
certificate: bufferToString(certificate),
|
||||||
@@ -284,7 +457,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
serialNumber,
|
serialNumber,
|
||||||
certificateId: cert.id,
|
certificateId: cert.id,
|
||||||
projectId: profile.projectId,
|
projectId: profile.projectId,
|
||||||
profileName: profile.slug
|
profileName: profile.slug,
|
||||||
|
commonName: cert.commonName || ""
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -294,8 +468,6 @@ export const certificateV3ServiceFactory = ({
|
|||||||
validity,
|
validity,
|
||||||
notBefore,
|
notBefore,
|
||||||
notAfter,
|
notAfter,
|
||||||
signatureAlgorithm,
|
|
||||||
keyAlgorithm,
|
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -319,38 +491,40 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
validateCaSupport(ca, "CSR signing");
|
validateCaSupport(ca, "CSR signing");
|
||||||
|
|
||||||
if (!actorAuthMethod) {
|
|
||||||
throw new BadRequestError({ message: "Authentication method is required for certificate signing" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
templateId: profile.certificateTemplateId
|
templateId: profile.certificateTemplateId,
|
||||||
|
internal: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certificateRequest = extractCertificateRequestFromCSR(csr);
|
||||||
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||||
|
|
||||||
|
const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } =
|
||||||
|
extractAlgorithmsFromCSR(csr);
|
||||||
|
|
||||||
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
|
profile.certificateTemplateId,
|
||||||
|
mappedCertificateRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validationResult.isValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const effectiveSignatureAlgorithm = signatureAlgorithm;
|
const effectiveSignatureAlgorithm = extractedSignatureAlgorithm;
|
||||||
const effectiveKeyAlgorithm = keyAlgorithm;
|
const effectiveKeyAlgorithm = extractedKeyAlgorithm;
|
||||||
|
|
||||||
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Key algorithm is required by template policy but not provided in request"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Signature algorithm is required by template policy but not provided in request"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||||
await internalCaService.signCertFromCa({
|
await internalCaService.signCertFromCa({
|
||||||
@@ -371,7 +545,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, { profileId });
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
||||||
|
|
||||||
|
await certificateDAL.updateById(cert.id, {
|
||||||
|
profileId,
|
||||||
|
renewBeforeDays: finalRenewBeforeDays
|
||||||
|
});
|
||||||
|
|
||||||
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
||||||
const certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
const certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
||||||
@@ -383,7 +562,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
serialNumber,
|
serialNumber,
|
||||||
certificateId: cert.id,
|
certificateId: cert.id,
|
||||||
projectId: profile.projectId,
|
projectId: profile.projectId,
|
||||||
profileName: profile.slug
|
profileName: profile.slug,
|
||||||
|
commonName: cert.commonName || ""
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -479,9 +659,405 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renewCertificate = async ({
|
||||||
|
certificateId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
internal = false
|
||||||
|
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
|
||||||
|
const renewalResult = await certificateDAL.transaction(async (tx) => {
|
||||||
|
const originalCert = await certificateDAL.findById(certificateId, tx);
|
||||||
|
if (!originalCert) {
|
||||||
|
throw new NotFoundError({ message: "Certificate not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!originalCert.profileId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Only certificates issued from a profile can be renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm;
|
||||||
|
const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm;
|
||||||
|
|
||||||
|
if (!originalSignatureAlgorithm || !originalKeyAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
|
||||||
|
if (!profile) {
|
||||||
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx);
|
||||||
|
if (!certificateSecret) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message:
|
||||||
|
"Certificate is not eligible for renewal: certificates issued from CSR (external private key) cannot be renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!internal) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||||
|
ProjectPermissionSub.CertificateProfiles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const eligibilityCheck = validateRenewalEligibility(originalCert, ca);
|
||||||
|
if (!eligibilityCheck.isEligible) {
|
||||||
|
await certificateDAL.updateById(originalCert.id, {
|
||||||
|
renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
|
|
||||||
|
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
templateId: profile.certificateTemplateId,
|
||||||
|
internal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalTtlInDays = Math.ceil(
|
||||||
|
(new Date(originalCert.notAfter).getTime() - new Date(originalCert.notBefore).getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
const ttl = `${originalTtlInDays}d`;
|
||||||
|
|
||||||
|
const certificateRequest = {
|
||||||
|
commonName: originalCert.commonName || undefined,
|
||||||
|
keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
|
||||||
|
extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(
|
||||||
|
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
|
||||||
|
),
|
||||||
|
subjectAlternativeNames: originalCert.altNames
|
||||||
|
? originalCert.altNames.split(",").map((san) => {
|
||||||
|
const trimmed = san.trim();
|
||||||
|
|
||||||
|
const isIpv4 = new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed);
|
||||||
|
const isIpv6 = new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed);
|
||||||
|
if (isIpv4 || isIpv6) {
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||||
|
value: trimmed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new RE2("^[^@]+@[^@]+\\.[^@]+$").test(trimmed)) {
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.EMAIL,
|
||||||
|
value: trimmed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new RE2("^[a-zA-Z][a-zA-Z0-9+.-]*:").test(trimmed)) {
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.URI,
|
||||||
|
value: trimmed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.DNS_NAME,
|
||||||
|
value: trimmed
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
validity: {
|
||||||
|
ttl
|
||||||
|
},
|
||||||
|
signatureAlgorithm: originalCert.signatureAlgorithm || undefined,
|
||||||
|
keyAlgorithm: originalCert.keyAlgorithm || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
|
profile.certificateTemplateId,
|
||||||
|
certificateRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validationResult.isValid) {
|
||||||
|
await certificateDAL.updateById(originalCert.id, {
|
||||||
|
renewalError: `Template validation failed: ${validationResult.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate renewal failed. Errors: ${validationResult.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
const notBefore = new Date();
|
||||||
|
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, ttl, notAfter);
|
||||||
|
|
||||||
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||||
|
await internalCaService.issueCertFromCa({
|
||||||
|
caId: ca.id,
|
||||||
|
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
|
||||||
|
commonName: originalCert.commonName || "",
|
||||||
|
altNames: originalCert.altNames || "",
|
||||||
|
ttl,
|
||||||
|
notBefore: normalizeDateForApi(notBefore),
|
||||||
|
notAfter: normalizeDateForApi(notAfter),
|
||||||
|
keyUsages: parseKeyUsages(originalCert.keyUsages),
|
||||||
|
extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages),
|
||||||
|
signatureAlgorithm: originalSignatureAlgorithm,
|
||||||
|
keyAlgorithm: originalKeyAlgorithm,
|
||||||
|
isFromProfile: true,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
internal: true,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
|
||||||
|
if (!newCert) {
|
||||||
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateDAL.updateById(
|
||||||
|
newCert.id,
|
||||||
|
{
|
||||||
|
profileId: originalCert.profileId,
|
||||||
|
renewBeforeDays: finalRenewBeforeDays,
|
||||||
|
renewedFromCertificateId: originalCert.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateDAL.updateById(
|
||||||
|
originalCert.id,
|
||||||
|
{
|
||||||
|
renewedByCertificateId: newCert.id,
|
||||||
|
renewalError: null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
serialNumber,
|
||||||
|
newCert,
|
||||||
|
originalCert,
|
||||||
|
profile
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: renewalResult.certificate,
|
||||||
|
issuingCaCertificate: renewalResult.issuingCaCertificate,
|
||||||
|
certificateChain: renewalResult.certificateChain,
|
||||||
|
serialNumber: renewalResult.serialNumber,
|
||||||
|
certificateId: renewalResult.newCert.id,
|
||||||
|
projectId: renewalResult.profile.projectId,
|
||||||
|
profileName: renewalResult.profile.slug,
|
||||||
|
commonName: renewalResult.originalCert.commonName || ""
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRenewalConfig = async ({
|
||||||
|
certificateId,
|
||||||
|
renewBeforeDays,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateRenewalConfigDTO): Promise<TRenewalConfigResponse> => {
|
||||||
|
const certificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (!certificate) {
|
||||||
|
throw new NotFoundError({ message: "Certificate not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Edit,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!certificate.profileId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate is not eligible for auto-renewal: certificate was not issued from a profile"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId);
|
||||||
|
if (!profile) {
|
||||||
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateSecret = await certificateSecretDAL.findOne({ certId: certificate.id });
|
||||||
|
if (!certificateSecret) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message:
|
||||||
|
"Certificate is not eligible for auto-renewal: certificates issued from CSR (external private key) cannot be auto-renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificate.status !== CertStatus.ACTIVE) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate is not eligible for auto-renewal: certificate status is ${certificate.status}, must be active`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
if (certificate.notAfter <= now) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate is not eligible for auto-renewal: certificate has expired"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificate.revokedAt) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate is not eligible for auto-renewal: certificate has been revoked"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificate.renewedByCertificateId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate is not eligible for auto-renewal: certificate has already been renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTtlInDays = Math.ceil(
|
||||||
|
(new Date(certificate.notAfter).getTime() - new Date(certificate.notBefore).getTime()) / (24 * 60 * 60 * 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (renewBeforeDays >= certificateTtlInDays) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid renewal configuration: renewal threshold exceeds certificate validity period"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidRenewalTiming(renewBeforeDays, new Date(certificate.notAfter))) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid renewal configuration: renewal would be triggered immediately or in the past"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateDAL.updateById(certificateId, {
|
||||||
|
renewBeforeDays
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
renewBeforeDays,
|
||||||
|
commonName: certificate.commonName || ""
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableRenewalConfig = async ({
|
||||||
|
certificateId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TDisableRenewalConfigDTO): Promise<TDisableRenewalResponse> => {
|
||||||
|
const certificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (!certificate) {
|
||||||
|
throw new NotFoundError({ message: "Certificate not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Edit,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!certificate.profileId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate is not eligible for auto-renewal: certificate was not issued from a profile"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await certificateProfileDAL.findByIdWithConfigs(certificate.profileId);
|
||||||
|
if (!profile) {
|
||||||
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Certificate is not eligible for auto-renewal: EST certificates cannot be auto-renewed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateDAL.updateById(certificateId, {
|
||||||
|
renewBeforeDays: null
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId: certificate.projectId,
|
||||||
|
commonName: certificate.commonName || ""
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
issueCertificateFromProfile,
|
issueCertificateFromProfile,
|
||||||
signCertificateFromProfile,
|
signCertificateFromProfile,
|
||||||
orderCertificateFromProfile
|
orderCertificateFromProfile,
|
||||||
|
renewCertificate,
|
||||||
|
updateRenewalConfig,
|
||||||
|
disableRenewalConfig
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user