diff --git a/.env.example b/.env.example
index bdb3e536d..67110d69a 100644
--- a/.env.example
+++ b/.env.example
@@ -63,3 +63,7 @@ CLIENT_SECRET_GITHUB_LOGIN=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=
+
+CAPTCHA_SECRET=
+
+NEXT_PUBLIC_CAPTCHA_SITE_KEY=
diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml
index 2086601a8..b6d698af4 100644
--- a/.github/workflows/check-api-for-breaking-changes.yml
+++ b/.github/workflows/check-api-for-breaking-changes.yml
@@ -35,11 +35,12 @@ jobs:
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
- docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
+ docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET -e ENCRYPTION_KEY=$ENCRYPTION_KEY --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
env:
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
JWT_AUTH_SECRET: something-random
+ ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- uses: actions/setup-go@v5
with:
go-version: '1.21.5'
@@ -73,4 +74,4 @@ jobs:
run: |
docker-compose -f "docker-compose.dev.yml" down
docker stop infisical-api
- docker remove infisical-api
\ No newline at end of file
+ docker remove infisical-api
diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml
index e4a5945e0..02c349237 100644
--- a/.github/workflows/release_build_infisical_cli.yml
+++ b/.github/workflows/release_build_infisical_cli.yml
@@ -22,6 +22,9 @@ jobs:
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
+ CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
+ CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
+ CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
goreleaser:
runs-on: ubuntu-20.04
@@ -56,7 +59,7 @@ jobs:
- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser-pro
- version: latest
+ version: v1.26.2-pro
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
diff --git a/.github/workflows/run-cli-tests.yml b/.github/workflows/run-cli-tests.yml
index e814f9143..f8e9d7797 100644
--- a/.github/workflows/run-cli-tests.yml
+++ b/.github/workflows/run-cli-tests.yml
@@ -20,7 +20,12 @@ on:
required: true
CLI_TESTS_ENV_SLUG:
required: true
-
+ CLI_TESTS_USER_EMAIL:
+ required: true
+ CLI_TESTS_USER_PASSWORD:
+ required: true
+ CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE:
+ required: true
jobs:
test:
defaults:
@@ -43,5 +48,8 @@ jobs:
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
+ CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
+ CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
+ INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
run: go test -v -count=1 ./test
diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical
index 737067534..8ffe7e3de 100644
--- a/Dockerfile.standalone-infisical
+++ b/Dockerfile.standalone-infisical
@@ -1,7 +1,7 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
-ARG SAML_ORG_SLUG=saml-org-slug-default
+ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:20-alpine AS base
@@ -36,8 +36,8 @@ ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
-ARG SAML_ORG_SLUG
-ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
+ARG CAPTCHA_SITE_KEY
+ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@@ -55,6 +55,7 @@ VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
+
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
@@ -93,9 +94,18 @@ RUN mkdir frontend-build
# Production stage
FROM base AS production
+RUN apk add --upgrade --no-cache ca-certificates
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
+# Give non-root-user permission to update SSL certs
+RUN chown -R non-root-user /etc/ssl/certs
+RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt
+RUN chmod -R u+rwx /etc/ssl/certs
+RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt
+RUN chown non-root-user /usr/sbin/update-ca-certificates
+RUN chmod u+rx /usr/sbin/update-ca-certificates
+
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
@@ -103,9 +113,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
-ARG SAML_ORG_SLUG
-ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
- BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
+ARG CAPTCHA_SITE_KEY
+ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
+ BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /
diff --git a/README.md b/README.md
index 80f754c02..e3e0ae521 100644
--- a/README.md
+++ b/README.md
@@ -48,25 +48,26 @@
## Introduction
-**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
+**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI.
-We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
+We're on a mission to make security tooling more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
## Features
-- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
-- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
-- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
-- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
-- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
+- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
+- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
+- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
+- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
+- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
-- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
+- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
-- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
-- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
-- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
+- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
+- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
+- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
-- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
+- **[Internal PKI](https://infisical.com/docs/documentation/platform/pki/private-ca)** to create Private CA hierarchies and start issuing and managing X.509 digital certificates.
+- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
And much more.
@@ -74,9 +75,9 @@ And much more.
Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/introduction)
-| Use Infisical Cloud | Deploy Infisical on premise |
-| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| The fastest and most reliable way to
get started with Infisical is signing up
for free to [Infisical Cloud](https://app.infisical.com/login). |
View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
+| Use Infisical Cloud | Deploy Infisical on premise |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
+| The fastest and most reliable way to
get started with Infisical is signing up
for free to [Infisical Cloud](https://app.infisical.com/login). |
View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
### Run Infisical locally
@@ -85,13 +86,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
Linux/macOS:
```console
-git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
+git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker compose -f docker-compose.prod.yml up
```
Windows Command Prompt:
```console
-git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
+git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker compose -f docker-compose.prod.yml up
```
Create an account at `http://localhost:80`
diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts
index c85244129..05753995c 100644
--- a/backend/e2e-test/mocks/keystore.ts
+++ b/backend/e2e-test/mocks/keystore.ts
@@ -1,4 +1,5 @@
import { TKeyStoreFactory } from "@app/keystore/keystore";
+import { Lock } from "@app/lib/red-lock";
export const mockKeyStore = (): TKeyStoreFactory => {
const store: Record = {};
@@ -25,6 +26,12 @@ export const mockKeyStore = (): TKeyStoreFactory => {
},
incrementBy: async () => {
return 1;
- }
+ },
+ acquireLock: () => {
+ return Promise.resolve({
+ release: () => {}
+ }) as Promise;
+ },
+ waitTillReady: async () => {}
};
};
diff --git a/backend/package-lock.json b/backend/package-lock.json
index cdaee75f4..8bd4a98a0 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -25,6 +25,8 @@
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/x509": "^1.10.0",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "^2.2.1",
"@ucast/mongo2js": "^1.3.4",
@@ -36,6 +38,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
+ "cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
"fastify-plugin": "^4.5.1",
@@ -51,7 +54,7 @@
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
- "mysql2": "^3.9.7",
+ "mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",
@@ -2458,9 +2461,9 @@
}
},
"node_modules/@fastify/session": {
- "version": "10.7.0",
- "resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.7.0.tgz",
- "integrity": "sha512-ECA75gnyaxcyIukgyO2NGT3XdbLReNl/pTKrrkRfDc6pVqNtdptwwfx9KXrIMOfsO4B3m84eF3wZ9GgnebiZ4w==",
+ "version": "10.9.0",
+ "resolved": "https://registry.npmjs.org/@fastify/session/-/session-10.9.0.tgz",
+ "integrity": "sha512-u/c42RuAaxCeEuRCAwK2+/SfGqKOd0NSyRzEvDwFBWySQoKUZQyb9OmmJSWJBbOP1OfaU2OsDrjbPbghE1l/YQ==",
"dependencies": {
"fastify-plugin": "^4.0.0",
"safe-stable-stringify": "^2.3.1"
@@ -3298,6 +3301,149 @@
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
},
+ "node_modules/@peculiar/asn1-cms": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
+ "integrity": "sha512-Wtk9R7yQxGaIaawHorWKP2OOOm/RZzamOmSWwaqGphIuU6TcKYih0slL6asZlSSZtVoYTrBfrddSOD/jTu9vuQ==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "@peculiar/asn1-x509-attr": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-csr": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.3.8.tgz",
+ "integrity": "sha512-ZmAaP2hfzgIGdMLcot8gHTykzoI+X/S53x1xoGbTmratETIaAbSWMiPGvZmXRA0SNEIydpMkzYtq4fQBxN1u1w==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-ecc": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz",
+ "integrity": "sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-pfx": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.3.8.tgz",
+ "integrity": "sha512-XhdnCVznMmSmgy68B9pVxiZ1XkKoE1BjO4Hv+eUGiY1pM14msLsFZ3N7K46SoITIVZLq92kKkXpGiTfRjlNLyg==",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.3.8",
+ "@peculiar/asn1-pkcs8": "^2.3.8",
+ "@peculiar/asn1-rsa": "^2.3.8",
+ "@peculiar/asn1-schema": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs8": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.3.8.tgz",
+ "integrity": "sha512-rL8k2x59v8lZiwLRqdMMmOJ30GHt6yuHISFIuuWivWjAJjnxzZBVzMTQ72sknX5MeTSSvGwPmEFk2/N8+UztFQ==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs9": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.3.8.tgz",
+ "integrity": "sha512-+nONq5tcK7vm3qdY7ZKoSQGQjhJYMJbwJGbXLFOhmqsFIxEWyQPHyV99+wshOjpOjg0wUSSkEEzX2hx5P6EKeQ==",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.3.8",
+ "@peculiar/asn1-pfx": "^2.3.8",
+ "@peculiar/asn1-pkcs8": "^2.3.8",
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "@peculiar/asn1-x509-attr": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-rsa": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.8.tgz",
+ "integrity": "sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-schema": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz",
+ "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==",
+ "dependencies": {
+ "asn1js": "^3.0.5",
+ "pvtsutils": "^1.3.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.8.tgz",
+ "integrity": "sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "ipaddr.js": "^2.1.0",
+ "pvtsutils": "^1.3.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509-attr": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.3.8.tgz",
+ "integrity": "sha512-4Z8mSN95MOuX04Aku9BUyMdsMKtVQUqWnr627IheiWnwFoheUhX3R4Y2zh23M7m80r4/WG8MOAckRKc77IRv6g==",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "asn1js": "^3.0.5",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509/node_modules/ipaddr.js": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
+ "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@peculiar/x509": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz",
+ "integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.3.8",
+ "@peculiar/asn1-csr": "^2.3.8",
+ "@peculiar/asn1-ecc": "^2.3.8",
+ "@peculiar/asn1-pkcs9": "^2.3.8",
+ "@peculiar/asn1-rsa": "^2.3.8",
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/asn1-x509": "^2.3.8",
+ "pvtsutils": "^1.3.5",
+ "reflect-metadata": "^0.2.2",
+ "tslib": "^2.6.2",
+ "tsyringe": "^4.8.0"
+ }
+ },
"node_modules/@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
@@ -4806,6 +4952,11 @@
"long": "*"
}
},
+ "node_modules/@types/luxon": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
+ "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
+ },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -5948,6 +6099,19 @@
"safer-buffer": "~2.1.0"
}
},
+ "node_modules/asn1js": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
+ "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
+ "dependencies": {
+ "pvtsutils": "^1.3.2",
+ "pvutils": "^1.1.3",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
@@ -6295,12 +6459,12 @@
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -6689,6 +6853,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "node_modules/cron": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz",
+ "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==",
+ "dependencies": {
+ "@types/luxon": "~3.4.0",
+ "luxon": "~3.4.0"
+ }
+ },
"node_modules/cron-parser": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
@@ -7942,9 +8115,9 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -10290,9 +10463,10 @@
}
},
"node_modules/mysql2": {
- "version": "3.9.7",
- "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
- "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
+ "version": "3.9.8",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.8.tgz",
+ "integrity": "sha512-+5JKNjPuks1FNMoy9TYpl77f+5frbTklz7eb3XDwbpsERRLEeXiW2PDEkakYF50UuKU2qwfGnyXpKYvukv8mGA==",
+ "license": "MIT",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -11701,6 +11875,22 @@
"node": ">=6"
}
},
+ "node_modules/pvtsutils": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz",
+ "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==",
+ "dependencies": {
+ "tslib": "^2.6.1"
+ }
+ },
+ "node_modules/pvutils": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
+ "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@@ -11882,6 +12072,11 @@
"node": ">=4"
}
},
+ "node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -13665,6 +13860,22 @@
"fsevents": "~2.3.3"
}
},
+ "node_modules/tsyringe": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz",
+ "integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==",
+ "dependencies": {
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/tsyringe/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
diff --git a/backend/package.json b/backend/package.json
index 85538d45a..f2a8582d6 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -86,6 +86,8 @@
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
+ "@peculiar/asn1-schema": "^2.3.8",
+ "@peculiar/x509": "^1.10.0",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "^2.2.1",
"@ucast/mongo2js": "^1.3.4",
@@ -97,6 +99,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
+ "cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
"fastify-plugin": "^4.5.1",
@@ -112,7 +115,7 @@
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
- "mysql2": "^3.9.7",
+ "mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",
diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts
index 8c913991f..43984ecfa 100644
--- a/backend/scripts/generate-schema-types.ts
+++ b/backend/scripts/generate-schema-types.ts
@@ -35,6 +35,8 @@ const getZodPrimitiveType = (type: string) => {
return "z.coerce.number()";
case "text":
return "z.string()";
+ case "bytea":
+ return "zodBuffer";
default:
throw new Error(`Invalid type: ${type}`);
}
@@ -96,10 +98,15 @@ const main = async () => {
const columnNames = Object.keys(columns);
let schema = "";
+ const zodImportSet = new Set();
for (let colNum = 0; colNum < columnNames.length; colNum++) {
const columnName = columnNames[colNum];
const colInfo = columns[columnName];
let ztype = getZodPrimitiveType(colInfo.type);
+ if (["zodBuffer"].includes(ztype)) {
+ zodImportSet.add(ztype);
+ }
+
// don't put optional on id
if (colInfo.defaultValue && columnName !== "id") {
const { defaultValue } = colInfo;
@@ -121,6 +128,8 @@ const main = async () => {
.split("_")
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
+ const zodImports = Array.from(zodImportSet);
+
// the insert and update are changed to zod input type to use default cases
writeFileSync(
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
@@ -131,6 +140,8 @@ const main = async () => {
import { z } from "zod";
+${zodImports.length ? `import { ${zodImports.join(",")} } from \"@app/lib/zod\";` : ""}
+
import { TImmutableDBKeys } from "./models";
export const ${pascalCase}Schema = z.object({${schema}});
diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 4776e26dc..e5cf3f79f 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -6,6 +6,7 @@ import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-ap
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
+import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
@@ -14,6 +15,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
+import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -29,10 +31,13 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
+import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
+import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
+import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@@ -51,6 +56,8 @@ import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
+import { TSecretReplicationServiceFactory } from "@app/services/secret-replication/secret-replication-service";
+import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
@@ -106,6 +113,7 @@ declare module "fastify" {
projectKey: TProjectKeyServiceFactory;
projectRole: TProjectRoleServiceFactory;
secret: TSecretServiceFactory;
+ secretReplication: TSecretReplicationServiceFactory;
secretTag: TSecretTagServiceFactory;
secretImport: TSecretImportServiceFactory;
projectBot: TProjectBotServiceFactory;
@@ -121,6 +129,7 @@ declare module "fastify" {
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory;
+ identityAzureAuth: TIdentityAzureAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
@@ -132,6 +141,9 @@ declare module "fastify" {
ldap: TLdapConfigServiceFactory;
auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory;
+ certificate: TCertificateServiceFactory;
+ certificateAuthority: TCertificateAuthorityServiceFactory;
+ certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;
@@ -141,6 +153,8 @@ declare module "fastify" {
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
+ secretSharing: TSecretSharingServiceFactory;
+ rateLimit: TRateLimitServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts
index 291197b0b..79342ea27 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -32,6 +32,27 @@ import {
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate,
+ TCertificateAuthorities,
+ TCertificateAuthoritiesInsert,
+ TCertificateAuthoritiesUpdate,
+ TCertificateAuthorityCerts,
+ TCertificateAuthorityCertsInsert,
+ TCertificateAuthorityCertsUpdate,
+ TCertificateAuthorityCrl,
+ TCertificateAuthorityCrlInsert,
+ TCertificateAuthorityCrlUpdate,
+ TCertificateAuthoritySecret,
+ TCertificateAuthoritySecretInsert,
+ TCertificateAuthoritySecretUpdate,
+ TCertificateBodies,
+ TCertificateBodiesInsert,
+ TCertificateBodiesUpdate,
+ TCertificates,
+ TCertificateSecrets,
+ TCertificateSecretsInsert,
+ TCertificateSecretsUpdate,
+ TCertificatesInsert,
+ TCertificatesUpdate,
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate,
@@ -62,6 +83,9 @@ import {
TIdentityAwsAuths,
TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate,
+ TIdentityAzureAuths,
+ TIdentityAzureAuthsInsert,
+ TIdentityAzureAuthsUpdate,
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
@@ -95,6 +119,15 @@ import {
TIntegrations,
TIntegrationsInsert,
TIntegrationsUpdate,
+ TKmsKeys,
+ TKmsKeysInsert,
+ TKmsKeysUpdate,
+ TKmsKeyVersions,
+ TKmsKeyVersionsInsert,
+ TKmsKeyVersionsUpdate,
+ TKmsRootConfig,
+ TKmsRootConfigInsert,
+ TKmsRootConfigUpdate,
TLdapConfigs,
TLdapConfigsInsert,
TLdapConfigsUpdate,
@@ -137,6 +170,9 @@ import {
TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate,
+ TRateLimit,
+ TRateLimitInsert,
+ TRateLimitUpdate,
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
@@ -173,6 +209,9 @@ import {
TSecretImports,
TSecretImportsInsert,
TSecretImportsUpdate,
+ TSecretReferences,
+ TSecretReferencesInsert,
+ TSecretReferencesUpdate,
TSecretRotationOutputs,
TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate,
@@ -183,6 +222,9 @@ import {
TSecretScanningGitRisks,
TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate,
+ TSecretSharing,
+ TSecretSharingInsert,
+ TSecretSharingUpdate,
TSecretsInsert,
TSecretSnapshotFolders,
TSecretSnapshotFoldersInsert,
@@ -234,12 +276,42 @@ import {
TWebhooksInsert,
TWebhooksUpdate
} from "@app/db/schemas";
-import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: Knex.CompositeTableType;
[TableName.Groups]: Knex.CompositeTableType;
+ [TableName.CertificateAuthority]: Knex.CompositeTableType<
+ TCertificateAuthorities,
+ TCertificateAuthoritiesInsert,
+ TCertificateAuthoritiesUpdate
+ >;
+ [TableName.CertificateAuthorityCert]: Knex.CompositeTableType<
+ TCertificateAuthorityCerts,
+ TCertificateAuthorityCertsInsert,
+ TCertificateAuthorityCertsUpdate
+ >;
+ [TableName.CertificateAuthoritySecret]: Knex.CompositeTableType<
+ TCertificateAuthoritySecret,
+ TCertificateAuthoritySecretInsert,
+ TCertificateAuthoritySecretUpdate
+ >;
+ [TableName.CertificateAuthorityCrl]: Knex.CompositeTableType<
+ TCertificateAuthorityCrl,
+ TCertificateAuthorityCrlInsert,
+ TCertificateAuthorityCrlUpdate
+ >;
+ [TableName.Certificate]: Knex.CompositeTableType;
+ [TableName.CertificateBody]: Knex.CompositeTableType<
+ TCertificateBodies,
+ TCertificateBodiesInsert,
+ TCertificateBodiesUpdate
+ >;
+ [TableName.CertificateSecret]: Knex.CompositeTableType<
+ TCertificateSecrets,
+ TCertificateSecretsInsert,
+ TCertificateSecretsUpdate
+ >;
[TableName.UserGroupMembership]: Knex.CompositeTableType<
TUserGroupMembership,
TUserGroupMembershipInsert,
@@ -325,6 +397,8 @@ declare module "knex/types/tables" {
TSecretFolderVersionsInsert,
TSecretFolderVersionsUpdate
>;
+ [TableName.SecretSharing]: Knex.CompositeTableType;
+ [TableName.RateLimit]: Knex.CompositeTableType;
[TableName.SecretTag]: Knex.CompositeTableType;
[TableName.SecretImport]: Knex.CompositeTableType;
[TableName.Integration]: Knex.CompositeTableType;
@@ -356,6 +430,11 @@ declare module "knex/types/tables" {
TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate
>;
+ [TableName.IdentityAzureAuth]: Knex.CompositeTableType<
+ TIdentityAzureAuths,
+ TIdentityAzureAuthsInsert,
+ TIdentityAzureAuthsUpdate
+ >;
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
@@ -502,5 +581,13 @@ declare module "knex/types/tables" {
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate
>;
+ // KMS service
+ [TableName.KmsServerRootConfig]: Knex.CompositeTableType<
+ TKmsRootConfig,
+ TKmsRootConfigInsert,
+ TKmsRootConfigUpdate
+ >;
+ [TableName.KmsKey]: Knex.CompositeTableType;
+ [TableName.KmsKeyVersion]: Knex.CompositeTableType;
}
}
diff --git a/backend/src/db/migrations/20240527073740_identity-azure-auth.ts b/backend/src/db/migrations/20240527073740_identity-azure-auth.ts
new file mode 100644
index 000000000..3d91b2f9c
--- /dev/null
+++ b/backend/src/db/migrations/20240527073740_identity-azure-auth.ts
@@ -0,0 +1,29 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.IdentityAzureAuth))) {
+ await knex.schema.createTable(TableName.IdentityAzureAuth, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
+ t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
+ t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
+ t.jsonb("accessTokenTrustedIps").notNullable();
+ t.timestamps(true, true, true);
+ t.uuid("identityId").notNullable().unique();
+ t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
+ t.string("tenantId").notNullable();
+ t.string("resource").notNullable();
+ t.string("allowedServicePrincipalIds").notNullable();
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.IdentityAzureAuth);
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.IdentityAzureAuth);
+ await dropOnUpdateTrigger(knex, TableName.IdentityAzureAuth);
+}
diff --git a/backend/src/db/migrations/20240528153905_add-user-account-mfa-locking.ts b/backend/src/db/migrations/20240528153905_add-user-account-mfa-locking.ts
new file mode 100644
index 000000000..2b2ecd783
--- /dev/null
+++ b/backend/src/db/migrations/20240528153905_add-user-account-mfa-locking.ts
@@ -0,0 +1,43 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
+ const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
+ const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
+
+ await knex.schema.alterTable(TableName.Users, (t) => {
+ if (!hasConsecutiveFailedMfaAttempts) {
+ t.integer("consecutiveFailedMfaAttempts").defaultTo(0);
+ }
+
+ if (!hasIsLocked) {
+ t.boolean("isLocked").defaultTo(false);
+ }
+
+ if (!hasTemporaryLockDateEnd) {
+ t.dateTime("temporaryLockDateEnd").nullable();
+ }
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
+ const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
+ const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
+
+ await knex.schema.alterTable(TableName.Users, (t) => {
+ if (hasConsecutiveFailedMfaAttempts) {
+ t.dropColumn("consecutiveFailedMfaAttempts");
+ }
+
+ if (hasIsLocked) {
+ t.dropColumn("isLocked");
+ }
+
+ if (hasTemporaryLockDateEnd) {
+ t.dropColumn("temporaryLockDateEnd");
+ }
+ });
+}
diff --git a/backend/src/db/migrations/20240528190137_secret_sharing.ts b/backend/src/db/migrations/20240528190137_secret_sharing.ts
new file mode 100644
index 000000000..c1eab2ea6
--- /dev/null
+++ b/backend/src/db/migrations/20240528190137_secret_sharing.ts
@@ -0,0 +1,29 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.SecretSharing))) {
+ await knex.schema.createTable(TableName.SecretSharing, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.string("name").notNullable();
+ t.text("encryptedValue").notNullable();
+ t.text("iv").notNullable();
+ t.text("tag").notNullable();
+ t.text("hashedHex").notNullable();
+ t.timestamp("expiresAt").notNullable();
+ t.uuid("userId").notNullable();
+ t.uuid("orgId").notNullable();
+ t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
+ t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
+ t.timestamps(true, true, true);
+ });
+
+ await createOnUpdateTrigger(knex, TableName.SecretSharing);
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.SecretSharing);
+}
diff --git a/backend/src/db/migrations/20240529060752_snap-shot-secret-index-secretversionid.ts b/backend/src/db/migrations/20240529060752_snap-shot-secret-index-secretversionid.ts
new file mode 100644
index 000000000..8d4322b5a
--- /dev/null
+++ b/backend/src/db/migrations/20240529060752_snap-shot-secret-index-secretversionid.ts
@@ -0,0 +1,21 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const doesSecretVersionIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "secretVersionId");
+ if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
+ await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
+ if (doesSecretVersionIdExist) t.index("secretVersionId");
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ const doesSecretVersionIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "secretVersionId");
+ if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
+ await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
+ if (doesSecretVersionIdExist) t.dropIndex("secretVersionId");
+ });
+ }
+}
diff --git a/backend/src/db/migrations/20240529203152_secret_sharing.ts b/backend/src/db/migrations/20240529203152_secret_sharing.ts
new file mode 100644
index 000000000..c1eab2ea6
--- /dev/null
+++ b/backend/src/db/migrations/20240529203152_secret_sharing.ts
@@ -0,0 +1,29 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.SecretSharing))) {
+ await knex.schema.createTable(TableName.SecretSharing, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.string("name").notNullable();
+ t.text("encryptedValue").notNullable();
+ t.text("iv").notNullable();
+ t.text("tag").notNullable();
+ t.text("hashedHex").notNullable();
+ t.timestamp("expiresAt").notNullable();
+ t.uuid("userId").notNullable();
+ t.uuid("orgId").notNullable();
+ t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
+ t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
+ t.timestamps(true, true, true);
+ });
+
+ await createOnUpdateTrigger(knex, TableName.SecretSharing);
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.SecretSharing);
+}
diff --git a/backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts b/backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts
new file mode 100644
index 000000000..e23d134db
--- /dev/null
+++ b/backend/src/db/migrations/20240530044702_universal-text-in-secret-sharing.ts
@@ -0,0 +1,33 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews");
+ const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name");
+
+ await knex.schema.alterTable(TableName.SecretSharing, (t) => {
+ if (!hasExpiresAfterViewsColumn) {
+ t.integer("expiresAfterViews");
+ }
+
+ if (hasSecretNameColumn) {
+ t.dropColumn("name");
+ }
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews");
+ const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name");
+
+ await knex.schema.alterTable(TableName.SecretSharing, (t) => {
+ if (hasExpiresAfterViewsColumn) {
+ t.dropColumn("expiresAfterViews");
+ }
+
+ if (!hasSecretNameColumn) {
+ t.string("name").notNullable();
+ }
+ });
+}
diff --git a/backend/src/db/migrations/20240531220007_secret-replication.ts b/backend/src/db/migrations/20240531220007_secret-replication.ts
new file mode 100644
index 000000000..ddb965df4
--- /dev/null
+++ b/backend/src/db/migrations/20240531220007_secret-replication.ts
@@ -0,0 +1,85 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const doesSecretImportIsReplicationExist = await knex.schema.hasColumn(TableName.SecretImport, "isReplication");
+ const doesSecretImportIsReplicationSuccessExist = await knex.schema.hasColumn(
+ TableName.SecretImport,
+ "isReplicationSuccess"
+ );
+ const doesSecretImportReplicationStatusExist = await knex.schema.hasColumn(
+ TableName.SecretImport,
+ "replicationStatus"
+ );
+ const doesSecretImportLastReplicatedExist = await knex.schema.hasColumn(TableName.SecretImport, "lastReplicated");
+ const doesSecretImportIsReservedExist = await knex.schema.hasColumn(TableName.SecretImport, "isReserved");
+
+ if (await knex.schema.hasTable(TableName.SecretImport)) {
+ await knex.schema.alterTable(TableName.SecretImport, (t) => {
+ if (!doesSecretImportIsReplicationExist) t.boolean("isReplication").defaultTo(false);
+ if (!doesSecretImportIsReplicationSuccessExist) t.boolean("isReplicationSuccess").nullable();
+ if (!doesSecretImportReplicationStatusExist) t.text("replicationStatus").nullable();
+ if (!doesSecretImportLastReplicatedExist) t.datetime("lastReplicated").nullable();
+ if (!doesSecretImportIsReservedExist) t.boolean("isReserved").defaultTo(false);
+ });
+ }
+
+ const doesSecretFolderReservedExist = await knex.schema.hasColumn(TableName.SecretFolder, "isReserved");
+ if (await knex.schema.hasTable(TableName.SecretFolder)) {
+ await knex.schema.alterTable(TableName.SecretFolder, (t) => {
+ if (!doesSecretFolderReservedExist) t.boolean("isReserved").defaultTo(false);
+ });
+ }
+
+ const doesSecretApprovalRequestIsReplicatedExist = await knex.schema.hasColumn(
+ TableName.SecretApprovalRequest,
+ "isReplicated"
+ );
+ if (await knex.schema.hasTable(TableName.SecretApprovalRequest)) {
+ await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
+ if (!doesSecretApprovalRequestIsReplicatedExist) t.boolean("isReplicated");
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ const doesSecretImportIsReplicationExist = await knex.schema.hasColumn(TableName.SecretImport, "isReplication");
+ const doesSecretImportIsReplicationSuccessExist = await knex.schema.hasColumn(
+ TableName.SecretImport,
+ "isReplicationSuccess"
+ );
+ const doesSecretImportReplicationStatusExist = await knex.schema.hasColumn(
+ TableName.SecretImport,
+ "replicationStatus"
+ );
+ const doesSecretImportLastReplicatedExist = await knex.schema.hasColumn(TableName.SecretImport, "lastReplicated");
+ const doesSecretImportIsReservedExist = await knex.schema.hasColumn(TableName.SecretImport, "isReserved");
+
+ if (await knex.schema.hasTable(TableName.SecretImport)) {
+ await knex.schema.alterTable(TableName.SecretImport, (t) => {
+ if (doesSecretImportIsReplicationExist) t.dropColumn("isReplication");
+ if (doesSecretImportIsReplicationSuccessExist) t.dropColumn("isReplicationSuccess");
+ if (doesSecretImportReplicationStatusExist) t.dropColumn("replicationStatus");
+ if (doesSecretImportLastReplicatedExist) t.dropColumn("lastReplicated");
+ if (doesSecretImportIsReservedExist) t.dropColumn("isReserved");
+ });
+ }
+
+ const doesSecretFolderReservedExist = await knex.schema.hasColumn(TableName.SecretFolder, "isReserved");
+ if (await knex.schema.hasTable(TableName.SecretFolder)) {
+ await knex.schema.alterTable(TableName.SecretFolder, (t) => {
+ if (doesSecretFolderReservedExist) t.dropColumn("isReserved");
+ });
+ }
+
+ const doesSecretApprovalRequestIsReplicatedExist = await knex.schema.hasColumn(
+ TableName.SecretApprovalRequest,
+ "isReplicated"
+ );
+ if (await knex.schema.hasTable(TableName.SecretApprovalRequest)) {
+ await knex.schema.alterTable(TableName.SecretApprovalRequest, (t) => {
+ if (doesSecretApprovalRequestIsReplicatedExist) t.dropColumn("isReplicated");
+ });
+ }
+}
diff --git a/backend/src/db/migrations/20240603075514_kms.ts b/backend/src/db/migrations/20240603075514_kms.ts
new file mode 100644
index 000000000..3531682d5
--- /dev/null
+++ b/backend/src/db/migrations/20240603075514_kms.ts
@@ -0,0 +1,56 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.KmsServerRootConfig))) {
+ await knex.schema.createTable(TableName.KmsServerRootConfig, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.binary("encryptedRootKey").notNullable();
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
+
+ if (!(await knex.schema.hasTable(TableName.KmsKey))) {
+ await knex.schema.createTable(TableName.KmsKey, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.binary("encryptedKey").notNullable();
+ t.string("encryptionAlgorithm").notNullable();
+ t.integer("version").defaultTo(1).notNullable();
+ t.string("description");
+ t.boolean("isDisabled").defaultTo(false);
+ t.boolean("isReserved").defaultTo(true);
+ t.string("projectId");
+ t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
+ t.uuid("orgId");
+ t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.KmsKey);
+
+ if (!(await knex.schema.hasTable(TableName.KmsKeyVersion))) {
+ await knex.schema.createTable(TableName.KmsKeyVersion, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.binary("encryptedKey").notNullable();
+ t.integer("version").notNullable();
+ t.uuid("kmsKeyId").notNullable();
+ t.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.KmsKeyVersion);
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.KmsServerRootConfig);
+ await dropOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
+
+ await knex.schema.dropTableIfExists(TableName.KmsKeyVersion);
+ await dropOnUpdateTrigger(knex, TableName.KmsKeyVersion);
+
+ await knex.schema.dropTableIfExists(TableName.KmsKey);
+ await dropOnUpdateTrigger(knex, TableName.KmsKey);
+}
diff --git a/backend/src/db/migrations/20240609133400_private-key-handoff.ts b/backend/src/db/migrations/20240609133400_private-key-handoff.ts
new file mode 100644
index 000000000..9741c6029
--- /dev/null
+++ b/backend/src/db/migrations/20240609133400_private-key-handoff.ts
@@ -0,0 +1,61 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
+ const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKey"
+ );
+ const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKeyIV"
+ );
+ const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKeyTag"
+ );
+ const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKeyEncoding"
+ );
+ if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
+ await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
+ if (!doesPasswordFieldExist) t.string("hashedPassword");
+ if (!doesPrivateKeyFieldExist) t.text("serverEncryptedPrivateKey");
+ if (!doesPrivateKeyIVFieldExist) t.text("serverEncryptedPrivateKeyIV");
+ if (!doesPrivateKeyTagFieldExist) t.text("serverEncryptedPrivateKeyTag");
+ if (!doesPrivateKeyEncodingFieldExist) t.text("serverEncryptedPrivateKeyEncoding");
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
+ const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKey"
+ );
+ const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKeyIV"
+ );
+ const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKeyTag"
+ );
+ const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
+ TableName.UserEncryptionKey,
+ "serverEncryptedPrivateKeyEncoding"
+ );
+ if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
+ await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
+ if (doesPasswordFieldExist) t.dropColumn("hashedPassword");
+ if (doesPrivateKeyFieldExist) t.dropColumn("serverEncryptedPrivateKey");
+ if (doesPrivateKeyIVFieldExist) t.dropColumn("serverEncryptedPrivateKeyIV");
+ if (doesPrivateKeyTagFieldExist) t.dropColumn("serverEncryptedPrivateKeyTag");
+ if (doesPrivateKeyEncodingFieldExist) t.dropColumn("serverEncryptedPrivateKeyEncoding");
+ });
+ }
+}
diff --git a/backend/src/db/migrations/20240610181521_add-consecutive-failed-password-attempts-user.ts b/backend/src/db/migrations/20240610181521_add-consecutive-failed-password-attempts-user.ts
new file mode 100644
index 000000000..66fa03182
--- /dev/null
+++ b/backend/src/db/migrations/20240610181521_add-consecutive-failed-password-attempts-user.ts
@@ -0,0 +1,29 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
+ TableName.Users,
+ "consecutiveFailedPasswordAttempts"
+ );
+
+ await knex.schema.alterTable(TableName.Users, (tb) => {
+ if (!hasConsecutiveFailedPasswordAttempts) {
+ tb.integer("consecutiveFailedPasswordAttempts").defaultTo(0);
+ }
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
+ TableName.Users,
+ "consecutiveFailedPasswordAttempts"
+ );
+
+ await knex.schema.alterTable(TableName.Users, (tb) => {
+ if (hasConsecutiveFailedPasswordAttempts) {
+ tb.dropColumn("consecutiveFailedPasswordAttempts");
+ }
+ });
+}
diff --git a/backend/src/db/migrations/20240612200518_add-pit-version-limit.ts b/backend/src/db/migrations/20240612200518_add-pit-version-limit.ts
new file mode 100644
index 000000000..e37c24e2c
--- /dev/null
+++ b/backend/src/db/migrations/20240612200518_add-pit-version-limit.ts
@@ -0,0 +1,21 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasPitVersionLimitColumn = await knex.schema.hasColumn(TableName.Project, "pitVersionLimit");
+ await knex.schema.alterTable(TableName.Project, (tb) => {
+ if (!hasPitVersionLimitColumn) {
+ tb.integer("pitVersionLimit").notNullable().defaultTo(10);
+ }
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ const hasPitVersionLimitColumn = await knex.schema.hasColumn(TableName.Project, "pitVersionLimit");
+ await knex.schema.alterTable(TableName.Project, (tb) => {
+ if (hasPitVersionLimitColumn) {
+ tb.dropColumn("pitVersionLimit");
+ }
+ });
+}
diff --git a/backend/src/db/migrations/20240614010847_custom-rate-limits-for-self-hosting.ts b/backend/src/db/migrations/20240614010847_custom-rate-limits-for-self-hosting.ts
new file mode 100644
index 000000000..c34b2d196
--- /dev/null
+++ b/backend/src/db/migrations/20240614010847_custom-rate-limits-for-self-hosting.ts
@@ -0,0 +1,31 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.RateLimit))) {
+ await knex.schema.createTable(TableName.RateLimit, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.integer("readRateLimit").defaultTo(600).notNullable();
+ t.integer("writeRateLimit").defaultTo(200).notNullable();
+ t.integer("secretsRateLimit").defaultTo(60).notNullable();
+ t.integer("authRateLimit").defaultTo(60).notNullable();
+ t.integer("inviteUserRateLimit").defaultTo(30).notNullable();
+ t.integer("mfaRateLimit").defaultTo(20).notNullable();
+ t.integer("creationLimit").defaultTo(30).notNullable();
+ t.integer("publicEndpointLimit").defaultTo(30).notNullable();
+ t.timestamps(true, true, true);
+ });
+
+ await createOnUpdateTrigger(knex, TableName.RateLimit);
+
+ // create init rate limit entry with defaults
+ await knex(TableName.RateLimit).insert({});
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.RateLimit);
+ await dropOnUpdateTrigger(knex, TableName.RateLimit);
+}
diff --git a/backend/src/db/migrations/20240614115952_tag-machine-identity.ts b/backend/src/db/migrations/20240614115952_tag-machine-identity.ts
new file mode 100644
index 000000000..fd11928b6
--- /dev/null
+++ b/backend/src/db/migrations/20240614115952_tag-machine-identity.ts
@@ -0,0 +1,25 @@
+import { Knex } from "knex";
+
+import { ActorType } from "@app/services/auth/auth-type";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType");
+ await knex.schema.alterTable(TableName.SecretTag, (tb) => {
+ if (!hasCreatedByActorType) {
+ tb.string("createdByActorType").notNullable().defaultTo(ActorType.USER);
+ tb.dropForeign("createdBy");
+ }
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType");
+ await knex.schema.alterTable(TableName.SecretTag, (tb) => {
+ if (hasCreatedByActorType) {
+ tb.dropColumn("createdByActorType");
+ tb.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("SET NULL");
+ }
+ });
+}
diff --git a/backend/src/db/migrations/20240614154212_certificate-mgmt.ts b/backend/src/db/migrations/20240614154212_certificate-mgmt.ts
new file mode 100644
index 000000000..a738a6b64
--- /dev/null
+++ b/backend/src/db/migrations/20240614154212_certificate-mgmt.ts
@@ -0,0 +1,137 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (await knex.schema.hasTable(TableName.Project)) {
+ const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId");
+ await knex.schema.alterTable(TableName.Project, (t) => {
+ if (!doesProjectCertificateKeyIdExist) {
+ t.uuid("kmsCertificateKeyId").nullable();
+ t.foreign("kmsCertificateKeyId").references("id").inTable(TableName.KmsKey);
+ }
+ });
+ }
+
+ if (!(await knex.schema.hasTable(TableName.CertificateAuthority))) {
+ await knex.schema.createTable(TableName.CertificateAuthority, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.timestamps(true, true, true);
+ t.uuid("parentCaId").nullable();
+ t.foreign("parentCaId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
+ t.string("projectId").notNullable();
+ t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
+ t.string("type").notNullable(); // root / intermediate
+ t.string("status").notNullable(); // active / pending-certificate
+ t.string("friendlyName").notNullable();
+ t.string("organization").notNullable();
+ t.string("ou").notNullable();
+ t.string("country").notNullable();
+ t.string("province").notNullable();
+ t.string("locality").notNullable();
+ t.string("commonName").notNullable();
+ t.string("dn").notNullable();
+ t.string("serialNumber").nullable().unique();
+ t.integer("maxPathLength").nullable();
+ t.string("keyAlgorithm").notNullable();
+ t.datetime("notBefore").nullable();
+ t.datetime("notAfter").nullable();
+ });
+ }
+
+ if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCert))) {
+ // table to keep track of certificates belonging to CA
+ await knex.schema.createTable(TableName.CertificateAuthorityCert, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.timestamps(true, true, true);
+ t.uuid("caId").notNullable().unique();
+ t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
+ t.binary("encryptedCertificate").notNullable();
+ t.binary("encryptedCertificateChain").notNullable();
+ });
+ }
+
+ if (!(await knex.schema.hasTable(TableName.CertificateAuthoritySecret))) {
+ await knex.schema.createTable(TableName.CertificateAuthoritySecret, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.timestamps(true, true, true);
+ t.uuid("caId").notNullable().unique();
+ t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
+ t.binary("encryptedPrivateKey").notNullable();
+ });
+ }
+
+ if (!(await knex.schema.hasTable(TableName.CertificateAuthorityCrl))) {
+ await knex.schema.createTable(TableName.CertificateAuthorityCrl, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.timestamps(true, true, true);
+ t.uuid("caId").notNullable().unique();
+ t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
+ t.binary("encryptedCrl").notNullable();
+ });
+ }
+
+ if (!(await knex.schema.hasTable(TableName.Certificate))) {
+ await knex.schema.createTable(TableName.Certificate, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.timestamps(true, true, true);
+ t.uuid("caId").notNullable();
+ t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
+ t.string("status").notNullable(); // active / pending-certificate
+ t.string("serialNumber").notNullable().unique();
+ t.string("friendlyName").notNullable();
+ t.string("commonName").notNullable();
+ t.datetime("notBefore").notNullable();
+ t.datetime("notAfter").notNullable();
+ t.datetime("revokedAt").nullable();
+ t.integer("revocationReason").nullable(); // integer based on crl reason in RFC 5280
+ });
+ }
+
+ if (!(await knex.schema.hasTable(TableName.CertificateBody))) {
+ await knex.schema.createTable(TableName.CertificateBody, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.timestamps(true, true, true);
+ t.uuid("certId").notNullable().unique();
+ t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
+ t.binary("encryptedCertificate").notNullable();
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.CertificateAuthority);
+ await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
+ await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret);
+ await createOnUpdateTrigger(knex, TableName.Certificate);
+ await createOnUpdateTrigger(knex, TableName.CertificateBody);
+}
+
+export async function down(knex: Knex): Promise {
+ // project
+ if (await knex.schema.hasTable(TableName.Project)) {
+ const doesProjectCertificateKeyIdExist = await knex.schema.hasColumn(TableName.Project, "kmsCertificateKeyId");
+ await knex.schema.alterTable(TableName.Project, (t) => {
+ if (doesProjectCertificateKeyIdExist) t.dropColumn("kmsCertificateKeyId");
+ });
+ }
+
+ // certificates
+ await knex.schema.dropTableIfExists(TableName.CertificateBody);
+ await dropOnUpdateTrigger(knex, TableName.CertificateBody);
+
+ await knex.schema.dropTableIfExists(TableName.Certificate);
+ await dropOnUpdateTrigger(knex, TableName.Certificate);
+
+ // certificate authorities
+ await knex.schema.dropTableIfExists(TableName.CertificateAuthoritySecret);
+ await dropOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret);
+
+ await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCrl);
+ await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCrl);
+
+ await knex.schema.dropTableIfExists(TableName.CertificateAuthorityCert);
+ await dropOnUpdateTrigger(knex, TableName.CertificateAuthorityCert);
+
+ await knex.schema.dropTableIfExists(TableName.CertificateAuthority);
+ await dropOnUpdateTrigger(knex, TableName.CertificateAuthority);
+}
diff --git a/backend/src/db/migrations/20240614184133_make-secret-sharing-public.ts b/backend/src/db/migrations/20240614184133_make-secret-sharing-public.ts
new file mode 100644
index 000000000..dc2756b74
--- /dev/null
+++ b/backend/src/db/migrations/20240614184133_make-secret-sharing-public.ts
@@ -0,0 +1,27 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
+ const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
+
+ if (await knex.schema.hasTable(TableName.SecretSharing)) {
+ await knex.schema.alterTable(TableName.SecretSharing, (t) => {
+ if (hasOrgIdColumn) t.uuid("orgId").nullable().alter();
+ if (hasUserIdColumn) t.uuid("userId").nullable().alter();
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
+ const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
+
+ if (await knex.schema.hasTable(TableName.SecretSharing)) {
+ await knex.schema.alterTable(TableName.SecretSharing, (t) => {
+ if (hasOrgIdColumn) t.uuid("orgId").notNullable().alter();
+ if (hasUserIdColumn) t.uuid("userId").notNullable().alter();
+ });
+ }
+}
diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts
new file mode 100644
index 000000000..16f303b5c
--- /dev/null
+++ b/backend/src/db/schemas/certificate-authorities.ts
@@ -0,0 +1,37 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const CertificateAuthoritiesSchema = z.object({
+ id: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ parentCaId: z.string().uuid().nullable().optional(),
+ projectId: z.string(),
+ type: z.string(),
+ status: z.string(),
+ friendlyName: z.string(),
+ organization: z.string(),
+ ou: z.string(),
+ country: z.string(),
+ province: z.string(),
+ locality: z.string(),
+ commonName: z.string(),
+ dn: z.string(),
+ serialNumber: z.string().nullable().optional(),
+ maxPathLength: z.number().nullable().optional(),
+ keyAlgorithm: z.string(),
+ notBefore: z.date().nullable().optional(),
+ notAfter: z.date().nullable().optional()
+});
+
+export type TCertificateAuthorities = z.infer;
+export type TCertificateAuthoritiesInsert = Omit, TImmutableDBKeys>;
+export type TCertificateAuthoritiesUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/certificate-authority-certs.ts b/backend/src/db/schemas/certificate-authority-certs.ts
new file mode 100644
index 000000000..96ad54f00
--- /dev/null
+++ b/backend/src/db/schemas/certificate-authority-certs.ts
@@ -0,0 +1,25 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const CertificateAuthorityCertsSchema = z.object({
+ id: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ caId: z.string().uuid(),
+ encryptedCertificate: zodBuffer,
+ encryptedCertificateChain: zodBuffer
+});
+
+export type TCertificateAuthorityCerts = z.infer;
+export type TCertificateAuthorityCertsInsert = Omit, TImmutableDBKeys>;
+export type TCertificateAuthorityCertsUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/certificate-authority-crl.ts b/backend/src/db/schemas/certificate-authority-crl.ts
new file mode 100644
index 000000000..204a0c60c
--- /dev/null
+++ b/backend/src/db/schemas/certificate-authority-crl.ts
@@ -0,0 +1,24 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const CertificateAuthorityCrlSchema = z.object({
+ id: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ caId: z.string().uuid(),
+ encryptedCrl: zodBuffer
+});
+
+export type TCertificateAuthorityCrl = z.infer;
+export type TCertificateAuthorityCrlInsert = Omit, TImmutableDBKeys>;
+export type TCertificateAuthorityCrlUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/certificate-authority-secret.ts b/backend/src/db/schemas/certificate-authority-secret.ts
new file mode 100644
index 000000000..36ab1c506
--- /dev/null
+++ b/backend/src/db/schemas/certificate-authority-secret.ts
@@ -0,0 +1,27 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const CertificateAuthoritySecretSchema = z.object({
+ id: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ caId: z.string().uuid(),
+ encryptedPrivateKey: zodBuffer
+});
+
+export type TCertificateAuthoritySecret = z.infer;
+export type TCertificateAuthoritySecretInsert = Omit<
+ z.input,
+ TImmutableDBKeys
+>;
+export type TCertificateAuthoritySecretUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/certificate-bodies.ts b/backend/src/db/schemas/certificate-bodies.ts
new file mode 100644
index 000000000..75afbddbd
--- /dev/null
+++ b/backend/src/db/schemas/certificate-bodies.ts
@@ -0,0 +1,22 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const CertificateBodiesSchema = z.object({
+ id: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ certId: z.string().uuid(),
+ encryptedCertificate: zodBuffer
+});
+
+export type TCertificateBodies = z.infer;
+export type TCertificateBodiesInsert = Omit, TImmutableDBKeys>;
+export type TCertificateBodiesUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/certificate-secrets.ts b/backend/src/db/schemas/certificate-secrets.ts
new file mode 100644
index 000000000..f8cad74f1
--- /dev/null
+++ b/backend/src/db/schemas/certificate-secrets.ts
@@ -0,0 +1,21 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const CertificateSecretsSchema = z.object({
+ id: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ certId: z.string().uuid(),
+ pk: z.string(),
+ sk: z.string()
+});
+
+export type TCertificateSecrets = z.infer;
+export type TCertificateSecretsInsert = Omit, TImmutableDBKeys>;
+export type TCertificateSecretsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts
new file mode 100644
index 000000000..b635420d5
--- /dev/null
+++ b/backend/src/db/schemas/certificates.ts
@@ -0,0 +1,27 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const CertificatesSchema = z.object({
+ id: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ caId: z.string().uuid(),
+ status: z.string(),
+ serialNumber: z.string(),
+ friendlyName: z.string(),
+ commonName: z.string(),
+ notBefore: z.date(),
+ notAfter: z.date(),
+ revokedAt: z.date().nullable().optional(),
+ revocationReason: z.number().nullable().optional()
+});
+
+export type TCertificates = z.infer;
+export type TCertificatesInsert = Omit, TImmutableDBKeys>;
+export type TCertificatesUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/identity-azure-auths.ts b/backend/src/db/schemas/identity-azure-auths.ts
new file mode 100644
index 000000000..856f7b8f1
--- /dev/null
+++ b/backend/src/db/schemas/identity-azure-auths.ts
@@ -0,0 +1,26 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const IdentityAzureAuthsSchema = z.object({
+ id: z.string().uuid(),
+ accessTokenTTL: z.coerce.number().default(7200),
+ accessTokenMaxTTL: z.coerce.number().default(7200),
+ accessTokenNumUsesLimit: z.coerce.number().default(0),
+ accessTokenTrustedIps: z.unknown(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ identityId: z.string().uuid(),
+ tenantId: z.string(),
+ resource: z.string(),
+ allowedServicePrincipalIds: z.string()
+});
+
+export type TIdentityAzureAuths = z.infer;
+export type TIdentityAzureAuthsInsert = Omit, TImmutableDBKeys>;
+export type TIdentityAzureAuthsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts
index cffa4f492..df126b7f0 100644
--- a/backend/src/db/schemas/index.ts
+++ b/backend/src/db/schemas/index.ts
@@ -8,6 +8,13 @@ export * from "./audit-logs";
export * from "./auth-token-sessions";
export * from "./auth-tokens";
export * from "./backup-private-key";
+export * from "./certificate-authorities";
+export * from "./certificate-authority-certs";
+export * from "./certificate-authority-crl";
+export * from "./certificate-authority-secret";
+export * from "./certificate-bodies";
+export * from "./certificate-secrets";
+export * from "./certificates";
export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
export * from "./git-app-install-sessions";
@@ -18,6 +25,7 @@ export * from "./groups";
export * from "./identities";
export * from "./identity-access-tokens";
export * from "./identity-aws-auths";
+export * from "./identity-azure-auths";
export * from "./identity-gcp-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-org-memberships";
@@ -29,6 +37,9 @@ export * from "./identity-universal-auths";
export * from "./incident-contacts";
export * from "./integration-auths";
export * from "./integrations";
+export * from "./kms-key-versions";
+export * from "./kms-keys";
+export * from "./kms-root-config";
export * from "./ldap-configs";
export * from "./ldap-group-maps";
export * from "./models";
@@ -44,6 +55,7 @@ export * from "./project-roles";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";
+export * from "./rate-limit";
export * from "./saml-configs";
export * from "./scim-tokens";
export * from "./secret-approval-policies";
@@ -56,9 +68,11 @@ export * from "./secret-blind-indexes";
export * from "./secret-folder-versions";
export * from "./secret-folders";
export * from "./secret-imports";
+export * from "./secret-references";
export * from "./secret-rotation-outputs";
export * from "./secret-rotations";
export * from "./secret-scanning-git-risks";
+export * from "./secret-sharing";
export * from "./secret-snapshot-folders";
export * from "./secret-snapshot-secrets";
export * from "./secret-snapshots";
diff --git a/backend/src/db/schemas/kms-key-versions.ts b/backend/src/db/schemas/kms-key-versions.ts
new file mode 100644
index 000000000..52a8069df
--- /dev/null
+++ b/backend/src/db/schemas/kms-key-versions.ts
@@ -0,0 +1,21 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const KmsKeyVersionsSchema = z.object({
+ id: z.string().uuid(),
+ encryptedKey: zodBuffer,
+ version: z.number(),
+ kmsKeyId: z.string().uuid()
+});
+
+export type TKmsKeyVersions = z.infer;
+export type TKmsKeyVersionsInsert = Omit, TImmutableDBKeys>;
+export type TKmsKeyVersionsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts
new file mode 100644
index 000000000..503c270d9
--- /dev/null
+++ b/backend/src/db/schemas/kms-keys.ts
@@ -0,0 +1,26 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const KmsKeysSchema = z.object({
+ id: z.string().uuid(),
+ encryptedKey: zodBuffer,
+ encryptionAlgorithm: z.string(),
+ version: z.number().default(1),
+ description: z.string().nullable().optional(),
+ isDisabled: z.boolean().default(false).nullable().optional(),
+ isReserved: z.boolean().default(true).nullable().optional(),
+ projectId: z.string().nullable().optional(),
+ orgId: z.string().uuid().nullable().optional()
+});
+
+export type TKmsKeys = z.infer;
+export type TKmsKeysInsert = Omit, TImmutableDBKeys>;
+export type TKmsKeysUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/kms-root-config.ts b/backend/src/db/schemas/kms-root-config.ts
new file mode 100644
index 000000000..d2c0edbc5
--- /dev/null
+++ b/backend/src/db/schemas/kms-root-config.ts
@@ -0,0 +1,19 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { zodBuffer } from "@app/lib/zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const KmsRootConfigSchema = z.object({
+ id: z.string().uuid(),
+ encryptedRootKey: zodBuffer
+});
+
+export type TKmsRootConfig = z.infer;
+export type TKmsRootConfigInsert = Omit, TImmutableDBKeys>;
+export type TKmsRootConfigUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index 28a6973b7..d7a5e6de1 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -2,6 +2,13 @@ import { z } from "zod";
export enum TableName {
Users = "users",
+ CertificateAuthority = "certificate_authorities",
+ CertificateAuthorityCert = "certificate_authority_certs",
+ CertificateAuthoritySecret = "certificate_authority_secret",
+ CertificateAuthorityCrl = "certificate_authority_crl",
+ Certificate = "certificates",
+ CertificateBody = "certificate_bodies",
+ CertificateSecret = "certificate_secrets",
Groups = "groups",
GroupProjectMembership = "group_project_memberships",
GroupProjectMembershipRole = "group_project_membership_roles",
@@ -18,6 +25,7 @@ export enum TableName {
IncidentContact = "incident_contacts",
UserAction = "user_actions",
SuperAdmin = "super_admin",
+ RateLimit = "rate_limit",
ApiKey = "api_keys",
Project = "projects",
ProjectBot = "project_bots",
@@ -29,6 +37,7 @@ export enum TableName {
ProjectKeys = "project_keys",
Secret = "secrets",
SecretReference = "secret_references",
+ SecretSharing = "secret_sharing",
SecretBlindIndex = "secret_blind_indexes",
SecretVersion = "secret_versions",
SecretFolder = "secret_folders",
@@ -47,6 +56,7 @@ export enum TableName {
IdentityUniversalAuth = "identity_universal_auths",
IdentityKubernetesAuth = "identity_kubernetes_auths",
IdentityGcpAuth = "identity_gcp_auths",
+ IdentityAzureAuth = "identity_azure_auths",
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths",
IdentityOrgMembership = "identity_org_memberships",
@@ -79,7 +89,11 @@ export enum TableName {
DynamicSecretLease = "dynamic_secret_leases",
// junction tables with tags
JnSecretTag = "secret_tag_junction",
- SecretVersionTag = "secret_version_tag_junction"
+ SecretVersionTag = "secret_version_tag_junction",
+ // KMS Service
+ KmsServerRootConfig = "kms_root_config",
+ KmsKey = "kms_keys",
+ KmsKeyVersion = "kms_key_versions"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
@@ -149,5 +163,6 @@ export enum IdentityAuthMethod {
Univeral = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
- AWS_AUTH = "aws-auth"
+ AWS_AUTH = "aws-auth",
+ AZURE_AUTH = "azure-auth"
}
diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts
index 3965e24c0..91035ab8e 100644
--- a/backend/src/db/schemas/projects.ts
+++ b/backend/src/db/schemas/projects.ts
@@ -16,7 +16,9 @@ export const ProjectsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
version: z.number().default(1),
- upgradeStatus: z.string().nullable().optional()
+ upgradeStatus: z.string().nullable().optional(),
+ kmsCertificateKeyId: z.string().uuid().nullable().optional(),
+ pitVersionLimit: z.number().default(10)
});
export type TProjects = z.infer;
diff --git a/backend/src/db/schemas/rate-limit.ts b/backend/src/db/schemas/rate-limit.ts
new file mode 100644
index 000000000..86b8776cc
--- /dev/null
+++ b/backend/src/db/schemas/rate-limit.ts
@@ -0,0 +1,26 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const RateLimitSchema = z.object({
+ id: z.string().uuid(),
+ readRateLimit: z.number().default(600),
+ writeRateLimit: z.number().default(200),
+ secretsRateLimit: z.number().default(60),
+ authRateLimit: z.number().default(60),
+ inviteUserRateLimit: z.number().default(30),
+ mfaRateLimit: z.number().default(20),
+ creationLimit: z.number().default(30),
+ publicEndpointLimit: z.number().default(30),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TRateLimit = z.infer;
+export type TRateLimitInsert = Omit, TImmutableDBKeys>;
+export type TRateLimitUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/secret-approval-requests.ts b/backend/src/db/schemas/secret-approval-requests.ts
index 6ee97fbb6..77ad370b7 100644
--- a/backend/src/db/schemas/secret-approval-requests.ts
+++ b/backend/src/db/schemas/secret-approval-requests.ts
@@ -18,7 +18,8 @@ export const SecretApprovalRequestsSchema = z.object({
statusChangeBy: z.string().uuid().nullable().optional(),
committerId: z.string().uuid(),
createdAt: z.date(),
- updatedAt: z.date()
+ updatedAt: z.date(),
+ isReplicated: z.boolean().nullable().optional()
});
export type TSecretApprovalRequests = z.infer;
diff --git a/backend/src/db/schemas/secret-folders.ts b/backend/src/db/schemas/secret-folders.ts
index 0f9684d0e..ad43ed1ad 100644
--- a/backend/src/db/schemas/secret-folders.ts
+++ b/backend/src/db/schemas/secret-folders.ts
@@ -14,7 +14,8 @@ export const SecretFoldersSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
envId: z.string().uuid(),
- parentId: z.string().uuid().nullable().optional()
+ parentId: z.string().uuid().nullable().optional(),
+ isReserved: z.boolean().default(false).nullable().optional()
});
export type TSecretFolders = z.infer;
diff --git a/backend/src/db/schemas/secret-imports.ts b/backend/src/db/schemas/secret-imports.ts
index 9d42d8da5..4bb1e669d 100644
--- a/backend/src/db/schemas/secret-imports.ts
+++ b/backend/src/db/schemas/secret-imports.ts
@@ -15,7 +15,12 @@ export const SecretImportsSchema = z.object({
position: z.number(),
createdAt: z.date(),
updatedAt: z.date(),
- folderId: z.string().uuid()
+ folderId: z.string().uuid(),
+ isReplication: z.boolean().default(false).nullable().optional(),
+ isReplicationSuccess: z.boolean().nullable().optional(),
+ replicationStatus: z.string().nullable().optional(),
+ lastReplicated: z.date().nullable().optional(),
+ isReserved: z.boolean().default(false).nullable().optional()
});
export type TSecretImports = z.infer;
diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts
new file mode 100644
index 000000000..c8d938861
--- /dev/null
+++ b/backend/src/db/schemas/secret-sharing.ts
@@ -0,0 +1,26 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const SecretSharingSchema = z.object({
+ id: z.string().uuid(),
+ encryptedValue: z.string(),
+ iv: z.string(),
+ tag: z.string(),
+ hashedHex: z.string(),
+ expiresAt: z.date(),
+ userId: z.string().uuid().nullable().optional(),
+ orgId: z.string().uuid().nullable().optional(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ expiresAfterViews: z.number().nullable().optional()
+});
+
+export type TSecretSharing = z.infer;
+export type TSecretSharingInsert = Omit, TImmutableDBKeys>;
+export type TSecretSharingUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/secret-tags.ts b/backend/src/db/schemas/secret-tags.ts
index f94e1e262..04bb7b752 100644
--- a/backend/src/db/schemas/secret-tags.ts
+++ b/backend/src/db/schemas/secret-tags.ts
@@ -15,7 +15,8 @@ export const SecretTagsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
createdBy: z.string().uuid().nullable().optional(),
- projectId: z.string()
+ projectId: z.string(),
+ createdByActorType: z.string().default("user")
});
export type TSecretTags = z.infer;
diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts
index 693b73b4c..fd9d21a9d 100644
--- a/backend/src/db/schemas/user-encryption-keys.ts
+++ b/backend/src/db/schemas/user-encryption-keys.ts
@@ -21,7 +21,12 @@ export const UserEncryptionKeysSchema = z.object({
tag: z.string(),
salt: z.string(),
verifier: z.string(),
- userId: z.string().uuid()
+ userId: z.string().uuid(),
+ hashedPassword: z.string().nullable().optional(),
+ serverEncryptedPrivateKey: z.string().nullable().optional(),
+ serverEncryptedPrivateKeyIV: z.string().nullable().optional(),
+ serverEncryptedPrivateKeyTag: z.string().nullable().optional(),
+ serverEncryptedPrivateKeyEncoding: z.string().nullable().optional()
});
export type TUserEncryptionKeys = z.infer;
diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts
index d5a4d5b49..5134f3ee6 100644
--- a/backend/src/db/schemas/users.ts
+++ b/backend/src/db/schemas/users.ts
@@ -22,7 +22,11 @@ export const UsersSchema = z.object({
updatedAt: z.date(),
isGhost: z.boolean().default(false),
username: z.string(),
- isEmailVerified: z.boolean().default(false).nullable().optional()
+ isEmailVerified: z.boolean().default(false).nullable().optional(),
+ consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
+ isLocked: z.boolean().default(false).nullable().optional(),
+ temporaryLockDateEnd: z.date().nullable().optional(),
+ consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
});
export type TUsers = z.infer;
diff --git a/backend/src/ee/routes/v1/certificate-authority-crl-router.ts b/backend/src/ee/routes/v1/certificate-authority-crl-router.ts
new file mode 100644
index 000000000..10792f508
--- /dev/null
+++ b/backend/src/ee/routes/v1/certificate-authority-crl-router.ts
@@ -0,0 +1,86 @@
+import { z } from "zod";
+
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
+import { readLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "GET",
+ url: "/:caId/crl",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get CRL of the CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
+ }),
+ response: {
+ 200: z.object({
+ crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
+ })
+ }
+ },
+ handler: async (req) => {
+ const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.GET_CA_CRL,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn
+ }
+ }
+ });
+
+ return {
+ crl
+ };
+ }
+ });
+
+ // server.route({
+ // method: "GET",
+ // url: "/:caId/crl/rotate",
+ // config: {
+ // rateLimit: writeLimit
+ // },
+ // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ // schema: {
+ // description: "Rotate CRL of the CA",
+ // params: z.object({
+ // caId: z.string().trim()
+ // }),
+ // response: {
+ // 200: z.object({
+ // message: z.string()
+ // })
+ // }
+ // },
+ // handler: async (req) => {
+ // await server.services.certificateAuthority.rotateCaCrl({
+ // caId: req.params.caId,
+ // actor: req.permission.type,
+ // actorId: req.permission.id,
+ // actorAuthMethod: req.permission.authMethod,
+ // actorOrgId: req.permission.orgId
+ // });
+ // return {
+ // message: "Successfully rotated CA CRL"
+ // };
+ // }
+ // });
+};
diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts
index 9a1a91672..58c6793d7 100644
--- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts
+++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts
@@ -5,10 +5,15 @@ import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
+import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
-import { ProjectPermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas";
+import {
+ ProjectPermissionSchema,
+ ProjectSpecificPrivilegePermissionSchema,
+ SanitizedIdentityPrivilegeSchema
+} from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@@ -39,7 +44,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
- permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
+ permissions: ProjectPermissionSchema.array()
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
+ .optional(),
+ privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
+ IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
+ ).optional()
}),
response: {
200: z.object({
@@ -49,6 +59,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
+ const { permissions, privilegePermission } = req.body;
+ if (!permissions && !privilegePermission) {
+ throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
+ }
+
+ const permission = privilegePermission
+ ? privilegePermission.actions.map((action) => ({
+ action,
+ subject: privilegePermission.subject,
+ conditions: privilegePermission.conditions
+ }))
+ : permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id,
actor: req.permission.type,
@@ -57,7 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false,
- permissions: JSON.stringify(packRules(req.body.permissions))
+ permissions: JSON.stringify(packRules(permission))
});
return { privilege };
}
@@ -90,7 +112,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
- permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
+ permissions: ProjectPermissionSchema.array()
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
+ .optional(),
+ privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
+ IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
+ ).optional(),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@@ -111,6 +138,19 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
+ const { permissions, privilegePermission } = req.body;
+ if (!permissions && !privilegePermission) {
+ throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
+ }
+
+ const permission = privilegePermission
+ ? privilegePermission.actions.map((action) => ({
+ action,
+ subject: privilegePermission.subject,
+ conditions: privilegePermission.conditions
+ }))
+ : permissions!;
+
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id,
actor: req.permission.type,
@@ -119,7 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true,
- permissions: JSON.stringify(packRules(req.body.permissions))
+ permissions: JSON.stringify(packRules(permission))
});
return { privilege };
}
@@ -156,13 +196,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
+ privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
+ IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
+ ).optional(),
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z
.string()
- .refine((val) => ms(val) > 0, "Temporary range must be a positive number")
+ .refine((val) => typeof val === "undefined" || ms(val) > 0, "Temporary range must be a positive number")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
temporaryAccessStartTime: z
.string()
@@ -179,7 +222,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
- const updatedInfo = req.body.privilegeDetails;
+ const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
+ if (!permissions && !privilegePermission) {
+ throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
+ }
+
+ const permission = privilegePermission
+ ? privilegePermission.actions.map((action) => ({
+ action,
+ subject: privilegePermission.subject,
+ conditions: privilegePermission.conditions
+ }))
+ : permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
actorId: req.permission.id,
actor: req.permission.type,
@@ -190,7 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: req.body.projectSlug,
data: {
...updatedInfo,
- permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined
+ permissions: permission ? JSON.stringify(packRules(permission)) : undefined
}
});
return { privilege };
diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts
index 16e23eb88..d04bd86fd 100644
--- a/backend/src/ee/routes/v1/index.ts
+++ b/backend/src/ee/routes/v1/index.ts
@@ -1,6 +1,7 @@
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
+import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerGroupRouter } from "./group-router";
@@ -10,6 +11,7 @@ import { registerLicenseRouter } from "./license-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
+import { registerRateLimitRouter } from "./rate-limit-router";
import { registerSamlRouter } from "./saml-router";
import { registerScimRouter } from "./scim-router";
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
@@ -45,6 +47,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
+ await server.register(registerRateLimitRouter, { prefix: "/rate-limit" });
await server.register(
async (dynamicSecretRouter) => {
@@ -54,6 +57,13 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/dynamic-secrets" }
);
+ await server.register(
+ async (pkiRouter) => {
+ await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
+ },
+ { prefix: "/pki" }
+ );
+
await server.register(registerSamlRouter, { prefix: "/sso" });
await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerLdapRouter, { prefix: "/ldap" });
diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts
index e146668c2..8cccbaac6 100644
--- a/backend/src/ee/routes/v1/ldap-router.ts
+++ b/backend/src/ee/routes/v1/ldap-router.ts
@@ -53,7 +53,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line
async (req: IncomingMessage, user, cb) => {
try {
- if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." });
+ if (!user.mail) throw new BadRequestError({ message: "Invalid request. Missing mail attribute on user." });
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
let groups: { dn: string; cn: string }[] | undefined;
diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts
index 380f61e23..6691032a8 100644
--- a/backend/src/ee/routes/v1/org-role-router.ts
+++ b/backend/src/ee/routes/v1/org-role-router.ts
@@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
.min(1)
.trim()
.refine(
- (val) => !Object.keys(OrgMembershipRole).includes(val),
+ (val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts
index bb4d2fa8e..69038a057 100644
--- a/backend/src/ee/routes/v1/project-role-router.ts
+++ b/backend/src/ee/routes/v1/project-role-router.ts
@@ -1,146 +1,232 @@
+import { packRules } from "@casl/ability/extra";
+import slugify from "@sindresorhus/slugify";
import { z } from "zod";
-import { ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
+import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
+import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
- url: "/:projectId/roles",
+ url: "/:projectSlug/roles",
config: {
rateLimit: writeLimit
},
schema: {
+ description: "Create a project role",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- projectId: z.string().trim()
+ projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
}),
body: z.object({
- slug: z.string().trim(),
- name: z.string().trim(),
- description: z.string().trim().optional(),
- permissions: z.any().array()
+ slug: z
+ .string()
+ .toLowerCase()
+ .trim()
+ .min(1)
+ .refine(
+ (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
+ "Please choose a different slug, the slug you have entered is reserved"
+ )
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid"
+ })
+ .describe(PROJECT_ROLE.CREATE.slug),
+ name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
+ description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
+ permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.CREATE.permissions)
}),
response: {
200: z.object({
- role: ProjectRolesSchema
+ role: SanitizedRoleSchema
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
- const role = await server.services.projectRole.createRole(
- req.permission.type,
- req.permission.id,
- req.params.projectId,
- req.body,
- req.permission.authMethod,
- req.permission.orgId
- );
+ const role = await server.services.projectRole.createRole({
+ actorAuthMethod: req.permission.authMethod,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actor: req.permission.type,
+ projectSlug: req.params.projectSlug,
+ data: {
+ ...req.body,
+ permissions: JSON.stringify(packRules(req.body.permissions))
+ }
+ });
return { role };
}
});
server.route({
method: "PATCH",
- url: "/:projectId/roles/:roleId",
+ url: "/:projectSlug/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
+ description: "Update a project role",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- projectId: z.string().trim(),
- roleId: z.string().trim()
+ projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
+ roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
- slug: z.string().trim().optional(),
- name: z.string().trim().optional(),
- description: z.string().trim().optional(),
- permissions: z.any().array()
+ slug: z
+ .string()
+ .toLowerCase()
+ .trim()
+ .optional()
+ .describe(PROJECT_ROLE.UPDATE.slug)
+ .refine(
+ (val) =>
+ typeof val === "undefined" ||
+ !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
+ "Please choose a different slug, the slug you have entered is reserved"
+ )
+ .refine((val) => typeof val === "undefined" || slugify(val) === val, {
+ message: "Slug must be a valid"
+ }),
+ name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
+ permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions)
}),
response: {
200: z.object({
- role: ProjectRolesSchema
+ role: SanitizedRoleSchema
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
- const role = await server.services.projectRole.updateRole(
- req.permission.type,
- req.permission.id,
- req.params.projectId,
- req.params.roleId,
- req.body,
- req.permission.authMethod,
- req.permission.orgId
- );
+ const role = await server.services.projectRole.updateRole({
+ actorAuthMethod: req.permission.authMethod,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actor: req.permission.type,
+ projectSlug: req.params.projectSlug,
+ roleId: req.params.roleId,
+ data: {
+ ...req.body,
+ permissions: JSON.stringify(packRules(req.body.permissions))
+ }
+ });
return { role };
}
});
server.route({
method: "DELETE",
- url: "/:projectId/roles/:roleId",
+ url: "/:projectSlug/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
+ description: "Delete a project role",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- projectId: z.string().trim(),
- roleId: z.string().trim()
+ projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
+ roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
}),
response: {
200: z.object({
- role: ProjectRolesSchema
+ role: SanitizedRoleSchema
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
- const role = await server.services.projectRole.deleteRole(
- req.permission.type,
- req.permission.id,
- req.params.projectId,
- req.params.roleId,
- req.permission.authMethod,
- req.permission.orgId
- );
+ const role = await server.services.projectRole.deleteRole({
+ actorAuthMethod: req.permission.authMethod,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actor: req.permission.type,
+ projectSlug: req.params.projectSlug,
+ roleId: req.params.roleId
+ });
return { role };
}
});
server.route({
method: "GET",
- url: "/:projectId/roles",
+ url: "/:projectSlug/roles",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ description: "List project role",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
+ }),
+ response: {
+ 200: z.object({
+ roles: ProjectRolesSchema.omit({ permissions: true }).array()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const roles = await server.services.projectRole.listRoles({
+ actorAuthMethod: req.permission.authMethod,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actor: req.permission.type,
+ projectSlug: req.params.projectSlug
+ });
+ return { roles };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:projectSlug/roles/slug/:slug",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
- projectId: z.string().trim()
+ projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
+ slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
}),
response: {
200: z.object({
- data: z.object({
- roles: ProjectRolesSchema.omit({ permissions: true })
- .merge(z.object({ permissions: z.unknown() }))
- .array()
- })
+ role: SanitizedRoleSchema
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
- const roles = await server.services.projectRole.listRoles(
- req.permission.type,
- req.permission.id,
- req.params.projectId,
- req.permission.authMethod,
- req.permission.orgId
- );
- return { data: { roles } };
+ const role = await server.services.projectRole.getRoleBySlug({
+ actorAuthMethod: req.permission.authMethod,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actor: req.permission.type,
+ projectSlug: req.params.projectSlug,
+ roleSlug: req.params.slug
+ });
+ return { role };
}
});
diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts
index 9795aaf86..8639ef1a1 100644
--- a/backend/src/ee/routes/v1/project-router.ts
+++ b/backend/src/ee/routes/v1/project-router.ts
@@ -143,7 +143,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
...req.query,
- startDate: req.query.endDate || getLastMidnightDateISO(),
+ endDate: req.query.endDate,
+ startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type
});
diff --git a/backend/src/ee/routes/v1/rate-limit-router.ts b/backend/src/ee/routes/v1/rate-limit-router.ts
new file mode 100644
index 000000000..2b08a0c32
--- /dev/null
+++ b/backend/src/ee/routes/v1/rate-limit-router.ts
@@ -0,0 +1,75 @@
+import { z } from "zod";
+
+import { RateLimitSchema } from "@app/db/schemas";
+import { BadRequestError } from "@app/lib/errors";
+import { readLimit } from "@app/server/config/rateLimiter";
+import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ response: {
+ 200: z.object({
+ rateLimit: RateLimitSchema
+ })
+ }
+ },
+ onRequest: (req, res, done) => {
+ verifyAuth([AuthMode.JWT])(req, res, () => {
+ verifySuperAdmin(req, res, done);
+ });
+ },
+ handler: async () => {
+ const rateLimit = await server.services.rateLimit.getRateLimits();
+ if (!rateLimit) {
+ throw new BadRequestError({
+ name: "Get Rate Limit Error",
+ message: "Rate limit configuration does not exist."
+ });
+ }
+ return { rateLimit };
+ }
+ });
+
+ server.route({
+ method: "PUT",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: (req, res, done) => {
+ verifyAuth([AuthMode.JWT])(req, res, () => {
+ verifySuperAdmin(req, res, done);
+ });
+ },
+
+ schema: {
+ body: z.object({
+ readRateLimit: z.number(),
+ writeRateLimit: z.number(),
+ secretsRateLimit: z.number(),
+ authRateLimit: z.number(),
+ inviteUserRateLimit: z.number(),
+ mfaRateLimit: z.number(),
+ creationLimit: z.number(),
+ publicEndpointLimit: z.number()
+ }),
+ response: {
+ 200: z.object({
+ rateLimit: RateLimitSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const rateLimit = await server.services.rateLimit.updateRateLimit(req.body);
+ return { rateLimit };
+ }
+ });
+};
diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts
index 8965c28f3..0a45486ef 100644
--- a/backend/src/ee/routes/v1/scim-router.ts
+++ b/backend/src/ee/routes/v1/scim-router.ts
@@ -362,6 +362,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const groups = await req.server.services.scim.listScimGroups({
orgId: req.permission.orgId,
startIndex: req.query.startIndex,
+ filter: req.query.filter,
limit: req.query.count
});
diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
index f6a955625..b09b58e26 100644
--- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
@@ -1,6 +1,7 @@
import { nanoid } from "nanoid";
import { z } from "zod";
+import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
@@ -19,7 +20,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
workspaceId: z.string(),
name: z.string().optional(),
environment: z.string(),
- secretPath: z.string().optional().nullable(),
+ secretPath: z
+ .string()
+ .optional()
+ .nullable()
+ .transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
@@ -63,7 +68,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
name: z.string().optional(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
- secretPath: z.string().optional().nullable()
+ secretPath: z
+ .string()
+ .optional()
+ .nullable()
+ .transform((val) => (val ? removeTrailingSlash(val) : val))
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
@@ -157,7 +166,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
querystring: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
- secretPath: z.string().trim()
+ secretPath: z.string().trim().transform(removeTrailingSlash)
}),
response: {
200: z.object({
diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts
index 2a9cc405d..b7204f72e 100644
--- a/backend/src/ee/routes/v1/secret-approval-request-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts
@@ -32,22 +32,20 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
}),
response: {
200: z.object({
- approvals: SecretApprovalRequestsSchema.merge(
- z.object({
- // secretPath: z.string(),
- policy: z.object({
- id: z.string(),
- name: z.string(),
- approvals: z.number(),
- approvers: z.string().array(),
- secretPath: z.string().optional().nullable()
- }),
- commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
- environment: z.string(),
- reviewers: z.object({ member: z.string(), status: z.string() }).array(),
- approvers: z.string().array()
- })
- ).array()
+ approvals: SecretApprovalRequestsSchema.extend({
+ // secretPath: z.string(),
+ policy: z.object({
+ id: z.string(),
+ name: z.string(),
+ approvals: z.number(),
+ approvers: z.string().array(),
+ secretPath: z.string().optional().nullable()
+ }),
+ commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
+ environment: z.string(),
+ reviewers: z.object({ member: z.string(), status: z.string() }).array(),
+ approvers: z.string().array()
+ }).array()
})
}
},
diff --git a/backend/src/ee/services/audit-log/audit-log-queue.ts b/backend/src/ee/services/audit-log/audit-log-queue.ts
index 6c563b573..f93b391a5 100644
--- a/backend/src/ee/services/audit-log/audit-log-queue.ts
+++ b/backend/src/ee/services/audit-log/audit-log-queue.ts
@@ -3,7 +3,6 @@ import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
-import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -113,35 +112,7 @@ export const auditLogQueueServiceFactory = ({
);
});
- queueService.start(QueueName.AuditLogPrune, async () => {
- logger.info(`${QueueName.AuditLogPrune}: queue task started`);
- await auditLogDAL.pruneAuditLog();
- logger.info(`${QueueName.AuditLogPrune}: queue task completed`);
- });
-
- // we do a repeat cron job in utc timezone at 12 Midnight each day
- const startAuditLogPruneJob = async () => {
- // clear previous job
- await queueService.stopRepeatableJob(
- QueueName.AuditLogPrune,
- QueueJobs.AuditLogPrune,
- { pattern: "0 0 * * *", utc: true },
- QueueName.AuditLogPrune // just a job id
- );
-
- await queueService.queue(QueueName.AuditLogPrune, QueueJobs.AuditLogPrune, undefined, {
- delay: 5000,
- jobId: QueueName.AuditLogPrune,
- repeat: { pattern: "0 0 * * *", utc: true }
- });
- };
-
- queueService.listen(QueueName.AuditLogPrune, "failed", (err) => {
- logger.error(err?.failedReason, `${QueueName.AuditLogPrune}: log pruning failed`);
- });
-
return {
- pushToLog,
- startAuditLogPruneJob
+ pushToLog
};
};
diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts
index e512389d7..8a0d2aef9 100644
--- a/backend/src/ee/services/audit-log/audit-log-types.ts
+++ b/backend/src/ee/services/audit-log/audit-log-types.ts
@@ -1,5 +1,6 @@
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
+import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
export type TListProjectAuditLogDTO = {
@@ -79,6 +80,10 @@ export enum EventType {
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
+ LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
+ ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
+ UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
+ GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
@@ -100,7 +105,21 @@ export enum EventType {
SECRET_APPROVAL_MERGED = "secret-approval-merged",
SECRET_APPROVAL_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
- SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
+ SECRET_APPROVAL_REOPENED = "secret-approval-reopened",
+ CREATE_CA = "create-certificate-authority",
+ GET_CA = "get-certificate-authority",
+ UPDATE_CA = "update-certificate-authority",
+ DELETE_CA = "delete-certificate-authority",
+ GET_CA_CSR = "get-certificate-authority-csr",
+ GET_CA_CERT = "get-certificate-authority-cert",
+ SIGN_INTERMEDIATE = "sign-intermediate",
+ IMPORT_CA_CERT = "import-certificate-authority-cert",
+ GET_CA_CRL = "get-certificate-authority-crl",
+ ISSUE_CERT = "issue-cert",
+ GET_CERT = "get-cert",
+ DELETE_CERT = "delete-cert",
+ REVOKE_CERT = "revoke-cert",
+ GET_CERT_BODY = "get-cert-body"
}
interface UserActorMetadata {
@@ -572,6 +591,48 @@ interface GetIdentityAwsAuthEvent {
};
}
+interface LoginIdentityAzureAuthEvent {
+ type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
+ metadata: {
+ identityId: string;
+ identityAzureAuthId: string;
+ identityAccessTokenId: string;
+ };
+}
+
+interface AddIdentityAzureAuthEvent {
+ type: EventType.ADD_IDENTITY_AZURE_AUTH;
+ metadata: {
+ identityId: string;
+ tenantId: string;
+ resource: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: Array;
+ };
+}
+
+interface UpdateIdentityAzureAuthEvent {
+ type: EventType.UPDATE_IDENTITY_AZURE_AUTH;
+ metadata: {
+ identityId: string;
+ tenantId?: string;
+ resource?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: Array;
+ };
+}
+
+interface GetIdentityAzureAuthEvent {
+ type: EventType.GET_IDENTITY_AZURE_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT;
metadata: {
@@ -797,6 +858,125 @@ interface SecretApprovalRequest {
};
}
+interface CreateCa {
+ type: EventType.CREATE_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCa {
+ type: EventType.GET_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface UpdateCa {
+ type: EventType.UPDATE_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ status: CaStatus;
+ };
+}
+
+interface DeleteCa {
+ type: EventType.DELETE_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCaCsr {
+ type: EventType.GET_CA_CSR;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCaCert {
+ type: EventType.GET_CA_CERT;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface SignIntermediate {
+ type: EventType.SIGN_INTERMEDIATE;
+ metadata: {
+ caId: string;
+ dn: string;
+ serialNumber: string;
+ };
+}
+
+interface ImportCaCert {
+ type: EventType.IMPORT_CA_CERT;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCaCrl {
+ type: EventType.GET_CA_CRL;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface IssueCert {
+ type: EventType.ISSUE_CERT;
+ metadata: {
+ caId: string;
+ dn: string;
+ serialNumber: string;
+ };
+}
+
+interface GetCert {
+ type: EventType.GET_CERT;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
+interface DeleteCert {
+ type: EventType.DELETE_CERT;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
+interface RevokeCert {
+ type: EventType.REVOKE_CERT;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
+interface GetCertBody {
+ type: EventType.GET_CERT_BODY;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -839,6 +1019,10 @@ export type Event =
| AddIdentityAwsAuthEvent
| UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent
+ | LoginIdentityAzureAuthEvent
+ | AddIdentityAzureAuthEvent
+ | UpdateIdentityAzureAuthEvent
+ | GetIdentityAzureAuthEvent
| CreateEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent
@@ -860,4 +1044,18 @@ export type Event =
| SecretApprovalMerge
| SecretApprovalClosed
| SecretApprovalRequest
- | SecretApprovalReopened;
+ | SecretApprovalReopened
+ | CreateCa
+ | GetCa
+ | UpdateCa
+ | DeleteCa
+ | GetCaCsr
+ | GetCaCert
+ | SignIntermediate
+ | ImportCaCert
+ | GetCaCrl
+ | IssueCert
+ | GetCert
+ | DeleteCert
+ | RevokeCert
+ | GetCertBody;
diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-dal.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-dal.ts
new file mode 100644
index 000000000..d367e1616
--- /dev/null
+++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-dal.ts
@@ -0,0 +1,10 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TCertificateAuthorityCrlDALFactory = ReturnType;
+
+export const certificateAuthorityCrlDALFactory = (db: TDbClient) => {
+ const caCrlOrm = ormify(db, TableName.CertificateAuthorityCrl);
+ return caCrlOrm;
+};
diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts
new file mode 100644
index 000000000..c8b56561e
--- /dev/null
+++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts
@@ -0,0 +1,172 @@
+import { ForbiddenError } from "@casl/ability";
+import * as x509 from "@peculiar/x509";
+
+import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
+import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
+import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import { BadRequestError } from "@app/lib/errors";
+import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
+import { TKmsServiceFactory } from "@app/services/kms/kms-service";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
+import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
+
+import { TGetCrl } from "./certificate-authority-crl-types";
+
+type TCertificateAuthorityCrlServiceFactoryDep = {
+ certificateAuthorityDAL: Pick;
+ certificateAuthorityCrlDAL: Pick;
+ projectDAL: Pick;
+ kmsService: Pick;
+ permissionService: Pick;
+ licenseService: Pick;
+};
+
+export type TCertificateAuthorityCrlServiceFactory = ReturnType;
+
+export const certificateAuthorityCrlServiceFactory = ({
+ certificateAuthorityDAL,
+ certificateAuthorityCrlDAL,
+ projectDAL,
+ kmsService,
+ permissionService,
+ licenseService
+}: TCertificateAuthorityCrlServiceFactoryDep) => {
+ /**
+ * Return the Certificate Revocation List (CRL) for CA with id [caId]
+ */
+ const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ const plan = await licenseService.getPlan(actorOrgId);
+ if (!plan.caCrl)
+ throw new BadRequestError({
+ message:
+ "Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
+ });
+
+ const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id });
+ if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const decryptedCrl = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caCrl.encryptedCrl
+ });
+
+ const crl = new x509.X509Crl(decryptedCrl);
+
+ const base64crl = crl.toString("base64");
+ const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
+
+ return {
+ crl: crlPem,
+ ca
+ };
+ };
+
+ // const rotateCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TRotateCrlDTO) => {
+ // const ca = await certificateAuthorityDAL.findById(caId);
+ // if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ // const { permission } = await permissionService.getProjectPermission(
+ // actor,
+ // actorId,
+ // ca.projectId,
+ // actorAuthMethod,
+ // actorOrgId
+ // );
+
+ // ForbiddenError.from(permission).throwUnlessCan(
+ // ProjectPermissionActions.Read,
+ // ProjectPermissionSub.CertificateAuthorities
+ // );
+
+ // const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
+
+ // const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
+
+ // const keyId = await getProjectKmsCertificateKeyId({
+ // projectId: ca.projectId,
+ // projectDAL,
+ // kmsService
+ // });
+
+ // const privateKey = await kmsService.decrypt({
+ // kmsId: keyId,
+ // cipherTextBlob: caSecret.encryptedPrivateKey
+ // });
+
+ // const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
+ // const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
+ // "sign"
+ // ]);
+
+ // const revokedCerts = await certificateDAL.find({
+ // caId: ca.id,
+ // status: CertStatus.REVOKED
+ // });
+
+ // const crl = await x509.X509CrlGenerator.create({
+ // issuer: ca.dn,
+ // thisUpdate: new Date(),
+ // nextUpdate: new Date("2025/12/12"),
+ // entries: revokedCerts.map((revokedCert) => {
+ // return {
+ // serialNumber: revokedCert.serialNumber,
+ // revocationDate: new Date(revokedCert.revokedAt as Date),
+ // reason: revokedCert.revocationReason as number,
+ // invalidity: new Date("2022/01/01"),
+ // issuer: ca.dn
+ // };
+ // }),
+ // signingAlgorithm: alg,
+ // signingKey: sk
+ // });
+
+ // const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
+ // kmsId: keyId,
+ // plainText: Buffer.from(new Uint8Array(crl.rawData))
+ // });
+
+ // await certificateAuthorityCrlDAL.update(
+ // {
+ // caId: ca.id
+ // },
+ // {
+ // encryptedCrl
+ // }
+ // );
+
+ // const base64crl = crl.toString("base64");
+ // const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
+
+ // return {
+ // crl: crlPem
+ // };
+ // };
+
+ return {
+ getCaCrl
+ // rotateCaCrl
+ };
+};
diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts
new file mode 100644
index 000000000..fc31e9eef
--- /dev/null
+++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-types.ts
@@ -0,0 +1,5 @@
+import { TProjectPermission } from "@app/lib/types";
+
+export type TGetCrl = {
+ caId: string;
+} & Omit;
diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts
index 6773c9486..8027f5907 100644
--- a/backend/src/ee/services/ldap-config/ldap-config-service.ts
+++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts
@@ -73,11 +73,17 @@ type TLdapConfigServiceFactoryDep = {
>;
userDAL: Pick<
TUserDALFactory,
- "create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find"
+ | "create"
+ | "findOne"
+ | "transaction"
+ | "updateById"
+ | "findUserEncKeyByUserIdsBatch"
+ | "find"
+ | "findUserEncKeyByUserId"
>;
userAliasDAL: Pick;
permissionService: Pick;
- licenseService: Pick;
+ licenseService: Pick;
};
export type TLdapConfigServiceFactory = ReturnType;
@@ -510,6 +516,7 @@ export const ldapConfigServiceFactory = ({
return newUserAlias;
});
}
+ await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
@@ -591,12 +598,14 @@ export const ldapConfigServiceFactory = ({
});
const isUserCompleted = Boolean(user.isAccepted);
+ const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
+ hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName,
lastName,
diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts
index b5cbf103e..ddbffba45 100644
--- a/backend/src/ee/services/license/__mocks__/licence-fns.ts
+++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts
@@ -25,6 +25,7 @@ export const getDefaultOnPremFeatures = () => {
trial_end: null,
has_used_trial: true,
secretApproval: false,
- secretRotation: true
+ secretRotation: true,
+ caCrl: false
};
};
diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts
index 189a3c4e0..9d2c5a472 100644
--- a/backend/src/ee/services/license/licence-fns.ts
+++ b/backend/src/ee/services/license/licence-fns.ts
@@ -34,7 +34,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
trial_end: null,
has_used_trial: true,
secretApproval: false,
- secretRotation: true
+ secretRotation: true,
+ caCrl: false
});
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts
index 4e70dfb5a..cf7048801 100644
--- a/backend/src/ee/services/license/license-dal.ts
+++ b/backend/src/ee/services/license/license-dal.ts
@@ -16,6 +16,8 @@ export const licenseDALFactory = (db: TDbClient) => {
void bd.where({ orgId });
}
})
+ .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
+ .where(`${TableName.Users}.isGhost`, false)
.count();
return doc?.[0].count;
} catch (error) {
diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts
index 47b46d010..46931468f 100644
--- a/backend/src/ee/services/license/license-service.ts
+++ b/backend/src/ee/services/license/license-service.ts
@@ -575,6 +575,9 @@ export const licenseServiceFactory = ({
getInstanceType() {
return instanceType;
},
+ get onPremFeatures() {
+ return onPremFeatures;
+ },
getPlan,
updateSubscriptionOrgMemberCount,
refreshPlan,
diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts
index 0c8fdc197..e23ff2c84 100644
--- a/backend/src/ee/services/license/license-types.ts
+++ b/backend/src/ee/services/license/license-types.ts
@@ -52,6 +52,7 @@ export type TFeatureSet = {
has_used_trial: true;
secretApproval: false;
secretRotation: true;
+ caCrl: false;
};
export type TOrgPlansTableDTO = {
diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts
index b24024bd4..4853faf61 100644
--- a/backend/src/ee/services/permission/project-permission.ts
+++ b/backend/src/ee/services/permission/project-permission.ts
@@ -26,7 +26,9 @@ export enum ProjectPermissionSub {
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
- Identity = "identity"
+ Identity = "identity",
+ CertificateAuthorities = "certificate-authorities",
+ Certificates = "certificates"
}
type SubjectFields = {
@@ -53,6 +55,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
+ | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
+ | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -139,6 +143,17 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
+ // double check if all CRUD are needed for CA and Certificates
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
+ can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
+ can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
+ can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
+
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
+ can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
+ can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
+ can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
+
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
@@ -205,6 +220,14 @@ const buildMemberPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
+ // double check if all CRUD are needed for CA and Certificates
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
+
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
+ can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
+ can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
+ can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
+
return rules;
};
@@ -229,6 +252,8 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
return rules;
};
diff --git a/backend/src/ee/services/rate-limit/rate-limit-dal.ts b/backend/src/ee/services/rate-limit/rate-limit-dal.ts
new file mode 100644
index 000000000..7279ff8ea
--- /dev/null
+++ b/backend/src/ee/services/rate-limit/rate-limit-dal.ts
@@ -0,0 +1,7 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TRateLimitDALFactory = ReturnType;
+
+export const rateLimitDALFactory = (db: TDbClient) => ormify(db, TableName.RateLimit, {});
diff --git a/backend/src/ee/services/rate-limit/rate-limit-service.ts b/backend/src/ee/services/rate-limit/rate-limit-service.ts
new file mode 100644
index 000000000..df90ca03f
--- /dev/null
+++ b/backend/src/ee/services/rate-limit/rate-limit-service.ts
@@ -0,0 +1,106 @@
+import { CronJob } from "cron";
+
+import { logger } from "@app/lib/logger";
+
+import { TLicenseServiceFactory } from "../license/license-service";
+import { TRateLimitDALFactory } from "./rate-limit-dal";
+import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
+
+let rateLimitMaxConfiguration = {
+ readLimit: 60,
+ publicEndpointLimit: 30,
+ writeLimit: 200,
+ secretsLimit: 60,
+ authRateLimit: 60,
+ inviteUserRateLimit: 30,
+ mfaRateLimit: 20,
+ creationLimit: 30
+};
+
+Object.freeze(rateLimitMaxConfiguration);
+
+export const getRateLimiterConfig = () => {
+ return rateLimitMaxConfiguration;
+};
+
+type TRateLimitServiceFactoryDep = {
+ rateLimitDAL: TRateLimitDALFactory;
+ licenseService: Pick;
+};
+
+export type TRateLimitServiceFactory = ReturnType;
+
+export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateLimitServiceFactoryDep) => {
+ const DEFAULT_RATE_LIMIT_CONFIG_ID = "00000000-0000-0000-0000-000000000000";
+
+ const getRateLimits = async (): Promise => {
+ let rateLimit: TRateLimit;
+
+ try {
+ rateLimit = await rateLimitDAL.findOne({ id: DEFAULT_RATE_LIMIT_CONFIG_ID });
+ if (!rateLimit) {
+ // rate limit might not exist
+ rateLimit = await rateLimitDAL.create({
+ // @ts-expect-error id is kept as fixed because there should only be one rate limit config per instance
+ id: DEFAULT_RATE_LIMIT_CONFIG_ID
+ });
+ }
+ return rateLimit;
+ } catch (err) {
+ logger.error("Error fetching rate limits %o", err);
+ return undefined;
+ }
+ };
+
+ const updateRateLimit = async (updates: TRateLimitUpdateDTO): Promise => {
+ return rateLimitDAL.updateById(DEFAULT_RATE_LIMIT_CONFIG_ID, updates);
+ };
+
+ const syncRateLimitConfiguration = async () => {
+ try {
+ const rateLimit = await getRateLimits();
+ if (rateLimit) {
+ const newRateLimitMaxConfiguration: typeof rateLimitMaxConfiguration = {
+ readLimit: rateLimit.readRateLimit,
+ publicEndpointLimit: rateLimit.publicEndpointLimit,
+ writeLimit: rateLimit.writeRateLimit,
+ secretsLimit: rateLimit.secretsRateLimit,
+ authRateLimit: rateLimit.authRateLimit,
+ inviteUserRateLimit: rateLimit.inviteUserRateLimit,
+ mfaRateLimit: rateLimit.mfaRateLimit,
+ creationLimit: rateLimit.creationLimit
+ };
+
+ logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
+ Object.freeze(newRateLimitMaxConfiguration);
+ rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
+ }
+ } catch (error) {
+ logger.error(`Error syncing rate limit configurations: %o`, error);
+ }
+ };
+
+ const initializeBackgroundSync = async () => {
+ if (!licenseService.onPremFeatures.customRateLimits) {
+ logger.info("Current license does not support custom rate limit configuration");
+ return;
+ }
+
+ logger.info("Setting up background sync process for rate limits");
+ // initial sync upon startup
+ await syncRateLimitConfiguration();
+
+ // sync rate limits configuration every 10 minutes
+ const job = new CronJob("*/10 * * * *", syncRateLimitConfiguration);
+ job.start();
+
+ return job;
+ };
+
+ return {
+ getRateLimits,
+ updateRateLimit,
+ initializeBackgroundSync,
+ syncRateLimitConfiguration
+ };
+};
diff --git a/backend/src/ee/services/rate-limit/rate-limit-types.ts b/backend/src/ee/services/rate-limit/rate-limit-types.ts
new file mode 100644
index 000000000..19519aafb
--- /dev/null
+++ b/backend/src/ee/services/rate-limit/rate-limit-types.ts
@@ -0,0 +1,16 @@
+export type TRateLimitUpdateDTO = {
+ readRateLimit: number;
+ writeRateLimit: number;
+ secretsRateLimit: number;
+ authRateLimit: number;
+ inviteUserRateLimit: number;
+ mfaRateLimit: number;
+ creationLimit: number;
+ publicEndpointLimit: number;
+};
+
+export type TRateLimit = {
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+} & TRateLimitUpdateDTO;
diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts
index 7dfd211e1..3cc51e1c2 100644
--- a/backend/src/ee/services/saml-config/saml-config-service.ts
+++ b/backend/src/ee/services/saml-config/saml-config-service.ts
@@ -41,7 +41,10 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f
type TSamlConfigServiceFactoryDep = {
samlConfigDAL: Pick;
- userDAL: Pick;
+ userDAL: Pick<
+ TUserDALFactory,
+ "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
+ >;
userAliasDAL: Pick;
orgDAL: Pick<
TOrgDALFactory,
@@ -50,7 +53,7 @@ type TSamlConfigServiceFactoryDep = {
orgMembershipDAL: Pick;
orgBotDAL: Pick;
permissionService: Pick;
- licenseService: Pick;
+ licenseService: Pick;
tokenService: Pick;
smtpService: Pick;
};
@@ -449,8 +452,10 @@ export const samlConfigServiceFactory = ({
return newUser;
});
}
+ await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted);
+ const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
@@ -463,6 +468,7 @@ export const samlConfigServiceFactory = ({
organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: authProvider,
+ hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
authType: UserAliasType.SAML,
isUserCompleted,
...(relayState
diff --git a/backend/src/ee/services/scim/scim-fns.ts b/backend/src/ee/services/scim/scim-fns.ts
index ec54a4d1f..08b652185 100644
--- a/backend/src/ee/services/scim/scim-fns.ts
+++ b/backend/src/ee/services/scim/scim-fns.ts
@@ -18,6 +18,20 @@ export const buildScimUserList = ({
};
};
+export const parseScimFilter = (filterToParse: string | undefined) => {
+ if (!filterToParse) return {};
+ const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
+
+ let attributeName = parsedName;
+ if (parsedName === "userName") {
+ attributeName = "email";
+ } else if (parsedName === "displayName") {
+ attributeName = "name";
+ }
+
+ return { [attributeName]: parsedValue.replace(/"/g, "") };
+};
+
export const buildScimUser = ({
orgMembershipId,
username,
diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts
index 9a084c6d7..dc175f15b 100644
--- a/backend/src/ee/services/scim/scim-service.ts
+++ b/backend/src/ee/services/scim/scim-service.ts
@@ -30,7 +30,7 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
-import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
+import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
import {
TCreateScimGroupDTO,
TCreateScimTokenDTO,
@@ -184,18 +184,6 @@ export const scimServiceFactory = ({
status: 403
});
- const parseFilter = (filterToParse: string | undefined) => {
- if (!filterToParse) return {};
- const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
-
- let attributeName = parsedName;
- if (parsedName === "userName") {
- attributeName = "email";
- }
-
- return { [attributeName]: parsedValue.replace(/"/g, "") };
- };
-
const findOpts = {
...(startIndex && { offset: startIndex - 1 }),
...(limit && { limit })
@@ -204,7 +192,7 @@ export const scimServiceFactory = ({
const users = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
- ...parseFilter(filter)
+ ...parseScimFilter(filter)
},
findOpts
);
@@ -391,7 +379,7 @@ export const scimServiceFactory = ({
);
}
}
-
+ await licenseService.updateSubscriptionOrgMemberCount(org.id);
return { user, orgMembership };
});
@@ -557,7 +545,7 @@ export const scimServiceFactory = ({
return {}; // intentionally return empty object upon success
};
- const listScimGroups = async ({ orgId, startIndex, limit }: TListScimGroupsDTO) => {
+ const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@@ -580,7 +568,8 @@ export const scimServiceFactory = ({
const groups = await groupDAL.findGroups(
{
- orgId
+ orgId,
+ ...(filter && parseScimFilter(filter))
},
{
offset: startIndex - 1,
diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts
index 46ab90b8f..cffc80407 100644
--- a/backend/src/ee/services/scim/scim-types.ts
+++ b/backend/src/ee/services/scim/scim-types.ts
@@ -66,6 +66,7 @@ export type TDeleteScimUserDTO = {
export type TListScimGroupsDTO = {
startIndex: number;
+ filter?: string;
limit: number;
orgId: string;
};
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
index 8ddadb9bf..f99384de6 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
@@ -4,6 +4,7 @@ import picomatch from "picomatch";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
+import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
@@ -207,7 +208,8 @@ export const secretApprovalPolicyServiceFactory = ({
return sapPolicies;
};
- const getSecretApprovalPolicy = async (projectId: string, environment: string, secretPath: string) => {
+ const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => {
+ const secretPath = removeTrailingSlash(path);
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) throw new BadRequestError({ message: "Environment not found" });
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index 690d308d2..5d0977134 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -15,9 +15,16 @@ import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
-import { getAllNestedSecretReferences } from "@app/services/secret/secret-fns";
+import {
+ fnSecretBlindIndexCheck,
+ fnSecretBlindIndexCheckV2,
+ fnSecretBulkDelete,
+ fnSecretBulkInsert,
+ fnSecretBulkUpdate,
+ getAllNestedSecretReferences
+} from "@app/services/secret/secret-fns";
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
-import { TSecretServiceFactory } from "@app/services/secret/secret-service";
+import { SecretOperations } from "@app/services/secret/secret-types";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
@@ -32,7 +39,6 @@ import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-requ
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
import {
ApprovalStatus,
- CommitType,
RequestState,
TApprovalRequestCountDTO,
TGenerateSecretApprovalRequestDTO,
@@ -45,10 +51,11 @@ import {
type TSecretApprovalRequestServiceFactoryDep = {
permissionService: Pick;
+ projectBotService: Pick;
secretApprovalRequestDAL: TSecretApprovalRequestDALFactory;
secretApprovalRequestSecretDAL: TSecretApprovalRequestSecretDALFactory;
secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory;
- folderDAL: Pick;
+ folderDAL: Pick;
secretDAL: TSecretDALFactory;
secretTagDAL: Pick;
secretBlindIndexDAL: Pick;
@@ -56,16 +63,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretVersionDAL: Pick;
secretVersionTagDAL: Pick;
projectDAL: Pick;
- projectBotService: Pick;
- secretService: Pick<
- TSecretServiceFactory,
- | "fnSecretBulkInsert"
- | "fnSecretBulkUpdate"
- | "fnSecretBlindIndexCheck"
- | "fnSecretBulkDelete"
- | "fnSecretBlindIndexCheckV2"
- >;
- secretQueueService: Pick;
+ secretQueueService: Pick;
};
export type TSecretApprovalRequestServiceFactory = ReturnType;
@@ -82,7 +80,6 @@ export const secretApprovalRequestServiceFactory = ({
projectDAL,
permissionService,
snapshotService,
- secretService,
secretVersionDAL,
secretQueueService,
projectBotService
@@ -302,11 +299,12 @@ export const secretApprovalRequestServiceFactory = ({
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
- const conflicts: Array<{ secretId: string; op: CommitType }> = [];
- let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Create);
+ const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
+ let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
if (secretCreationCommits.length) {
- const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await secretService.fnSecretBlindIndexCheckV2({
+ const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
folderId,
+ secretDAL,
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
@@ -319,17 +317,19 @@ export const secretApprovalRequestServiceFactory = ({
secretCreationCommits
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
.forEach((el) => {
- conflicts.push({ op: CommitType.Create, secretId: el.id });
+ conflicts.push({ op: SecretOperations.Create, secretId: el.id });
});
secretCreationCommits = secretCreationCommits.filter(
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
);
}
- let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Update);
+ let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
if (secretUpdationCommits.length) {
- const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await secretService.fnSecretBlindIndexCheckV2({
+ const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
folderId,
+ secretDAL,
+ userId: "",
inputSecrets: secretUpdationCommits
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
.map(({ secretBlindIndex }) => {
@@ -347,7 +347,7 @@ export const secretApprovalRequestServiceFactory = ({
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
)
.forEach((el) => {
- conflicts.push({ op: CommitType.Update, secretId: el.id });
+ conflicts.push({ op: SecretOperations.Update, secretId: el.id });
});
secretUpdationCommits = secretUpdationCommits.filter(
@@ -356,11 +356,11 @@ export const secretApprovalRequestServiceFactory = ({
);
}
- const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Delete);
+ const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
const botKey = await projectBotService.getBotKey(projectId).catch(() => null);
const mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
const newSecrets = secretCreationCommits.length
- ? await secretService.fnSecretBulkInsert({
+ ? await fnSecretBulkInsert({
tx,
folderId,
inputSecrets: secretCreationCommits.map((el) => ({
@@ -403,7 +403,7 @@ export const secretApprovalRequestServiceFactory = ({
})
: [];
const updatedSecrets = secretUpdationCommits.length
- ? await secretService.fnSecretBulkUpdate({
+ ? await fnSecretBulkUpdate({
folderId,
projectId,
tx,
@@ -449,11 +449,13 @@ export const secretApprovalRequestServiceFactory = ({
})
: [];
const deletedSecret = secretDeletionCommits.length
- ? await secretService.fnSecretBulkDelete({
+ ? await fnSecretBulkDelete({
projectId,
folderId,
tx,
actorId: "",
+ secretDAL,
+ secretQueueService,
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
if (!secretBlindIndex) {
throw new BadRequestError({
@@ -480,12 +482,14 @@ export const secretApprovalRequestServiceFactory = ({
};
});
await snapshotService.performSnapshot(folderId);
- const folder = await folderDAL.findById(folderId);
- // TODO(akhilmhdh-pg): change query to do secret path from folder
+ const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
await secretQueueService.syncSecrets({
projectId,
- secretPath: "/",
- environment: folder?.environment.envSlug as string
+ secretPath: folder.path,
+ environmentSlug: folder.environmentSlug,
+ actorId,
+ actor
});
return mergeStatus;
};
@@ -533,9 +537,9 @@ export const secretApprovalRequestServiceFactory = ({
const commits: Omit[] = [];
const commitTagIds: Record = {};
// for created secret approval change
- const createdSecrets = data[CommitType.Create];
+ const createdSecrets = data[SecretOperations.Create];
if (createdSecrets && createdSecrets?.length) {
- const { keyName2BlindIndex } = await secretService.fnSecretBlindIndexCheck({
+ const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets: createdSecrets,
folderId,
isNew: true,
@@ -546,7 +550,7 @@ export const secretApprovalRequestServiceFactory = ({
commits.push(
...createdSecrets.map(({ secretName, ...el }) => ({
...el,
- op: CommitType.Create as const,
+ op: SecretOperations.Create as const,
version: 1,
secretBlindIndex: keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM,
@@ -558,12 +562,12 @@ export const secretApprovalRequestServiceFactory = ({
});
}
// not secret approval for update operations
- const updatedSecrets = data[CommitType.Update];
+ const updatedSecrets = data[SecretOperations.Update];
if (updatedSecrets && updatedSecrets?.length) {
// get all blind index
// Find all those secrets
// if not throw not found
- const { keyName2BlindIndex, secrets: secretsToBeUpdated } = await secretService.fnSecretBlindIndexCheck({
+ const { keyName2BlindIndex, secrets: secretsToBeUpdated } = await fnSecretBlindIndexCheck({
inputSecrets: updatedSecrets,
folderId,
isNew: false,
@@ -574,8 +578,8 @@ export const secretApprovalRequestServiceFactory = ({
// now find any secret that needs to update its name
// same process as above
const nameUpdatedSecrets = updatedSecrets.filter(({ newSecretName }) => Boolean(newSecretName));
- const { keyName2BlindIndex: newKeyName2BlindIndex } = await secretService.fnSecretBlindIndexCheck({
- inputSecrets: nameUpdatedSecrets,
+ const { keyName2BlindIndex: newKeyName2BlindIndex } = await fnSecretBlindIndexCheck({
+ inputSecrets: nameUpdatedSecrets.map(({ newSecretName }) => ({ secretName: newSecretName as string })),
folderId,
isNew: true,
blindIndexCfg,
@@ -592,14 +596,14 @@ export const secretApprovalRequestServiceFactory = ({
const secretId = secsGroupedByBlindIndex[keyName2BlindIndex[secretName]][0].id;
const secretBlindIndex =
newSecretName && newKeyName2BlindIndex[newSecretName]
- ? newKeyName2BlindIndex?.[secretName]
+ ? newKeyName2BlindIndex?.[newSecretName]
: keyName2BlindIndex[secretName];
// add tags
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
return {
...latestSecretVersions[secretId],
...el,
- op: CommitType.Update as const,
+ op: SecretOperations.Update as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId].id,
secretBlindIndex,
@@ -609,12 +613,12 @@ export const secretApprovalRequestServiceFactory = ({
);
}
// deleted secrets
- const deletedSecrets = data[CommitType.Delete];
+ const deletedSecrets = data[SecretOperations.Delete];
if (deletedSecrets && deletedSecrets.length) {
// get all blind index
// Find all those secrets
// if not throw not found
- const { keyName2BlindIndex, secrets } = await secretService.fnSecretBlindIndexCheck({
+ const { keyName2BlindIndex, secrets } = await fnSecretBlindIndexCheck({
inputSecrets: deletedSecrets,
folderId,
isNew: false,
@@ -635,7 +639,7 @@ export const secretApprovalRequestServiceFactory = ({
if (!latestSecretVersions[secretId].secretBlindIndex)
throw new BadRequestError({ message: "Failed to find secret blind index" });
return {
- op: CommitType.Delete as const,
+ op: SecretOperations.Delete as const,
...latestSecretVersions[secretId],
secretBlindIndex: latestSecretVersions[secretId].secretBlindIndex as string,
secret: secretId,
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts
index 008b977e6..1fbb75418 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts
@@ -1,11 +1,6 @@
import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
-
-export enum CommitType {
- Create = "create",
- Update = "update",
- Delete = "delete"
-}
+import { SecretOperations } from "@app/services/secret/secret-types";
export enum RequestState {
Open = "open",
@@ -18,14 +13,14 @@ export enum ApprovalStatus {
REJECTED = "rejected"
}
-type TApprovalCreateSecret = Omit<
+export type TApprovalCreateSecret = Omit<
TSecretApprovalRequestsSecrets,
TImmutableDBKeys | "version" | "algorithm" | "keyEncoding" | "requestId" | "op" | "secretVersion" | "secretBlindIndex"
> & {
secretName: string;
tagIds?: string[];
};
-type TApprovalUpdateSecret = Partial & {
+export type TApprovalUpdateSecret = Partial & {
secretName: string;
newSecretName?: string;
tagIds?: string[];
@@ -36,9 +31,9 @@ export type TGenerateSecretApprovalRequestDTO = {
secretPath: string;
policy: TSecretApprovalPolicies;
data: {
- [CommitType.Create]?: TApprovalCreateSecret[];
- [CommitType.Update]?: TApprovalUpdateSecret[];
- [CommitType.Delete]?: { secretName: string }[];
+ [SecretOperations.Create]?: TApprovalCreateSecret[];
+ [SecretOperations.Update]?: TApprovalUpdateSecret[];
+ [SecretOperations.Delete]?: { secretName: string }[];
};
} & TProjectPermission;
diff --git a/backend/src/ee/services/secret-replication/secret-replication-constants.ts b/backend/src/ee/services/secret-replication/secret-replication-constants.ts
new file mode 100644
index 000000000..88c9ee166
--- /dev/null
+++ b/backend/src/ee/services/secret-replication/secret-replication-constants.ts
@@ -0,0 +1 @@
+export const MAX_REPLICATION_DEPTH = 5;
diff --git a/backend/src/ee/services/secret-replication/secret-replication-dal.ts b/backend/src/ee/services/secret-replication/secret-replication-dal.ts
new file mode 100644
index 000000000..3c4c021fd
--- /dev/null
+++ b/backend/src/ee/services/secret-replication/secret-replication-dal.ts
@@ -0,0 +1,10 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TSecretReplicationDALFactory = ReturnType;
+
+export const secretReplicationDALFactory = (db: TDbClient) => {
+ const orm = ormify(db, TableName.SecretVersion);
+ return orm;
+};
diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts
new file mode 100644
index 000000000..fd2f7cc1a
--- /dev/null
+++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts
@@ -0,0 +1,485 @@
+import { SecretType, TSecrets } from "@app/db/schemas";
+import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
+import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
+import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
+import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
+import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
+import { BadRequestError } from "@app/lib/errors";
+import { groupBy, unique } from "@app/lib/fn";
+import { logger } from "@app/lib/logger";
+import { alphaNumericNanoId } from "@app/lib/nanoid";
+import { QueueName, TQueueServiceFactory } from "@app/queue";
+import { ActorType } from "@app/services/auth/auth-type";
+import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
+import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
+import { TSecretDALFactory } from "@app/services/secret/secret-dal";
+import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
+import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
+import { SecretOperations } from "@app/services/secret/secret-types";
+import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
+import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
+import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
+import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
+import { ReservedFolders } from "@app/services/secret-folder/secret-folder-types";
+import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
+import { fnSecretsFromImports } from "@app/services/secret-import/secret-import-fns";
+import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
+
+import { MAX_REPLICATION_DEPTH } from "./secret-replication-constants";
+
+type TSecretReplicationServiceFactoryDep = {
+ secretDAL: Pick<
+ TSecretDALFactory,
+ "find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
+ >;
+ secretVersionDAL: Pick;
+ secretImportDAL: Pick;
+ folderDAL: Pick<
+ TSecretFolderDALFactory,
+ "findSecretPathByFolderIds" | "findBySecretPath" | "create" | "findOne" | "findByManySecretPath"
+ >;
+ secretVersionTagDAL: Pick;
+ secretQueueService: Pick;
+ queueService: Pick;
+ secretApprovalPolicyService: Pick;
+ keyStore: Pick;
+ secretBlindIndexDAL: Pick;
+ secretTagDAL: Pick;
+ secretApprovalRequestDAL: Pick;
+ projectMembershipDAL: Pick;
+ secretApprovalRequestSecretDAL: Pick<
+ TSecretApprovalRequestSecretDALFactory,
+ "insertMany" | "insertApprovalSecretTags"
+ >;
+ projectBotService: Pick;
+};
+
+export type TSecretReplicationServiceFactory = ReturnType;
+const SECRET_IMPORT_SUCCESS_LOCK = 10;
+
+const keystoreReplicationSuccessKey = (jobId: string, secretImportId: string) => `${jobId}-${secretImportId}`;
+const getReplicationKeyLockPrefix = (projectId: string, environmentSlug: string, secretPath: string) =>
+ `REPLICATION_SECRET_${projectId}-${environmentSlug}-${secretPath}`;
+export const getReplicationFolderName = (importId: string) => `${ReservedFolders.SecretReplication}${importId}`;
+
+const getDecryptedKeyValue = (key: string, secret: TSecrets) => {
+ const secretKey = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: secret.secretKeyCiphertext,
+ iv: secret.secretKeyIV,
+ tag: secret.secretKeyTag,
+ key
+ });
+
+ const secretValue = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: secret.secretValueCiphertext,
+ iv: secret.secretValueIV,
+ tag: secret.secretValueTag,
+ key
+ });
+ return { key: secretKey, value: secretValue };
+};
+
+export const secretReplicationServiceFactory = ({
+ secretDAL,
+ queueService,
+ secretVersionDAL,
+ secretImportDAL,
+ keyStore,
+ secretVersionTagDAL,
+ secretTagDAL,
+ folderDAL,
+ secretApprovalPolicyService,
+ secretApprovalRequestSecretDAL,
+ secretApprovalRequestDAL,
+ secretQueueService,
+ projectMembershipDAL,
+ projectBotService
+}: TSecretReplicationServiceFactoryDep) => {
+ const getReplicatedSecrets = (
+ botKey: string,
+ localSecrets: TSecrets[],
+ importedSecrets: { secrets: TSecrets[] }[]
+ ) => {
+ const deDupe = new Set();
+ const secrets = localSecrets
+ .filter(({ secretBlindIndex }) => Boolean(secretBlindIndex))
+ .map((el) => {
+ const decryptedSecret = getDecryptedKeyValue(botKey, el);
+ deDupe.add(decryptedSecret.key);
+ return { ...el, secretKey: decryptedSecret.key, secretValue: decryptedSecret.value };
+ });
+
+ for (let i = importedSecrets.length - 1; i >= 0; i = -1) {
+ importedSecrets[i].secrets.forEach((el) => {
+ const decryptedSecret = getDecryptedKeyValue(botKey, el);
+ if (deDupe.has(decryptedSecret.key) || !el.secretBlindIndex) {
+ return;
+ }
+ deDupe.add(decryptedSecret.key);
+ secrets.push({ ...el, secretKey: decryptedSecret.key, secretValue: decryptedSecret.value });
+ });
+ }
+ return secrets;
+ };
+
+ // IMPORTANT NOTE BEFORE READING THE FUNCTION
+ // SOURCE - Where secrets are copied from
+ // DESTINATION - Where the replicated imports that points to SOURCE from Destination
+ queueService.start(QueueName.SecretReplication, async (job) => {
+ logger.info(job.data, "Replication started");
+ const {
+ secretPath,
+ environmentSlug,
+ projectId,
+ actorId,
+ actor,
+ pickOnlyImportIds,
+ _deDupeReplicationQueue: deDupeReplicationQueue,
+ _deDupeQueue: deDupeQueue,
+ _depth: depth = 0
+ } = job.data;
+ if (depth > MAX_REPLICATION_DEPTH) return;
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
+ if (!folder) return;
+
+ // the the replicated imports made to the source. These are the destinations
+ const destinationSecretImports = await secretImportDAL.find({
+ importPath: secretPath,
+ importEnv: folder.envId
+ });
+
+ // CASE: normal mode <- link import <- replicated import
+ const nonReplicatedDestinationImports = destinationSecretImports.filter(({ isReplication }) => !isReplication);
+ if (nonReplicatedDestinationImports.length) {
+ // keep calling sync secret for all the imports made
+ const importedFolderIds = unique(nonReplicatedDestinationImports, (i) => i.folderId).map(
+ ({ folderId }) => folderId
+ );
+ const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
+ const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
+ await Promise.all(
+ nonReplicatedDestinationImports
+ .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
+ // filter out already synced ones
+ .filter(
+ ({ folderId }) =>
+ !deDupeQueue?.[
+ uniqueSecretQueueKey(
+ foldersGroupedById[folderId][0]?.environmentSlug as string,
+ foldersGroupedById[folderId][0]?.path as string
+ )
+ ]
+ )
+ .map(({ folderId }) =>
+ secretQueueService.replicateSecrets({
+ projectId,
+ secretPath: foldersGroupedById[folderId][0]?.path as string,
+ environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
+ actorId,
+ actor,
+ _depth: depth + 1,
+ _deDupeReplicationQueue: deDupeReplicationQueue,
+ _deDupeQueue: deDupeQueue
+ })
+ )
+ );
+ }
+
+ let destinationReplicatedSecretImports = destinationSecretImports.filter(({ isReplication }) =>
+ Boolean(isReplication)
+ );
+ destinationReplicatedSecretImports = pickOnlyImportIds
+ ? destinationReplicatedSecretImports.filter(({ id }) => pickOnlyImportIds?.includes(id))
+ : destinationReplicatedSecretImports;
+ if (!destinationReplicatedSecretImports.length) return;
+
+ const botKey = await projectBotService.getBotKey(projectId);
+
+ // these are the secrets to be added in replicated folders
+ const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared });
+ const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
+ const sourceImportedSecrets = await fnSecretsFromImports({
+ allowedImports: sourceSecretImports,
+ secretDAL,
+ folderDAL,
+ secretImportDAL
+ });
+ // secrets that gets replicated across imports
+ const sourceSecrets = getReplicatedSecrets(botKey, sourceLocalSecrets, sourceImportedSecrets);
+ const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string);
+
+ const lock = await keyStore.acquireLock(
+ [getReplicationKeyLockPrefix(projectId, environmentSlug, secretPath)],
+ 5000
+ );
+
+ try {
+ /* eslint-disable no-await-in-loop */
+ for (const destinationSecretImport of destinationReplicatedSecretImports) {
+ try {
+ const hasJobCompleted = await keyStore.getItem(
+ keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
+ KeyStorePrefixes.SecretReplication
+ );
+ if (hasJobCompleted) {
+ logger.info(
+ { jobId: job.id, importId: destinationSecretImport.id },
+ "Skipping this job as this has been successfully replicated."
+ );
+ // eslint-disable-next-line
+ continue;
+ }
+
+ const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
+ destinationSecretImport.folderId
+ ]);
+ if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" });
+
+ let destinationReplicationFolder = await folderDAL.findOne({
+ parentId: destinationFolder.id,
+ name: getReplicationFolderName(destinationSecretImport.id),
+ isReserved: true
+ });
+ if (!destinationReplicationFolder) {
+ destinationReplicationFolder = await folderDAL.create({
+ parentId: destinationFolder.id,
+ name: getReplicationFolderName(destinationSecretImport.id),
+ envId: destinationFolder.envId,
+ isReserved: true
+ });
+ }
+ const destinationReplicationFolderId = destinationReplicationFolder.id;
+
+ const destinationLocalSecretsFromDB = await secretDAL.find({
+ folderId: destinationReplicationFolderId
+ });
+ const destinationLocalSecrets = destinationLocalSecretsFromDB.map((el) => {
+ const decryptedSecret = getDecryptedKeyValue(botKey, el);
+ return { ...el, secretKey: decryptedSecret.key, secretValue: decryptedSecret.value };
+ });
+
+ const destinationLocalSecretsGroupedByBlindIndex = groupBy(
+ destinationLocalSecrets.filter(({ secretBlindIndex }) => Boolean(secretBlindIndex)),
+ (i) => i.secretBlindIndex as string
+ );
+
+ const locallyCreatedSecrets = sourceSecrets
+ .filter(
+ ({ secretBlindIndex }) => !destinationLocalSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]
+ )
+ .map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
+
+ const locallyUpdatedSecrets = sourceSecrets
+ .filter(
+ ({ secretBlindIndex, secretKey, secretValue }) =>
+ destinationLocalSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0] &&
+ // if key or value changed
+ (destinationLocalSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretKey !== secretKey ||
+ destinationLocalSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretValue !==
+ secretValue)
+ )
+ .map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
+
+ const locallyDeletedSecrets = destinationLocalSecrets
+ .filter(({ secretBlindIndex }) => !sourceSecretsGroupByBlindIndex[secretBlindIndex as string]?.[0])
+ .map((el) => ({ ...el, operation: SecretOperations.Delete }));
+
+ const isEmtpy =
+ locallyCreatedSecrets.length + locallyUpdatedSecrets.length + locallyDeletedSecrets.length === 0;
+ // eslint-disable-next-line
+ if (isEmtpy) continue;
+
+ const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
+ projectId,
+ destinationFolder.environmentSlug,
+ destinationFolder.path
+ );
+ // this means it should be a approval request rather than direct replication
+ if (policy && actor === ActorType.USER) {
+ const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId });
+ if (!membership) {
+ logger.error("Project membership not found in %s for user %s", projectId, actorId);
+ return;
+ }
+
+ const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id);
+ const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
+ destinationReplicationFolderId,
+ localSecretsLatestVersions
+ );
+ await secretApprovalRequestDAL.transaction(async (tx) => {
+ const approvalRequestDoc = await secretApprovalRequestDAL.create(
+ {
+ folderId: destinationReplicationFolderId,
+ slug: alphaNumericNanoId(),
+ policyId: policy.id,
+ status: "open",
+ hasMerged: false,
+ committerId: membership.id,
+ isReplicated: true
+ },
+ tx
+ );
+ const commits = locallyCreatedSecrets
+ .concat(locallyUpdatedSecrets)
+ .concat(locallyDeletedSecrets)
+ .map((doc) => {
+ const { operation } = doc;
+ const localSecret = destinationLocalSecretsGroupedByBlindIndex[doc.secretBlindIndex as string]?.[0];
+
+ return {
+ op: operation,
+ keyEncoding: doc.keyEncoding,
+ algorithm: doc.algorithm,
+ requestId: approvalRequestDoc.id,
+ metadata: doc.metadata,
+ secretKeyIV: doc.secretKeyIV,
+ secretKeyTag: doc.secretKeyTag,
+ secretKeyCiphertext: doc.secretKeyCiphertext,
+ secretValueIV: doc.secretValueIV,
+ secretValueTag: doc.secretValueTag,
+ secretValueCiphertext: doc.secretValueCiphertext,
+ secretBlindIndex: doc.secretBlindIndex,
+ secretCommentIV: doc.secretCommentIV,
+ secretCommentTag: doc.secretCommentTag,
+ secretCommentCiphertext: doc.secretCommentCiphertext,
+ skipMultilineEncoding: doc.skipMultilineEncoding,
+ // except create operation other two needs the secret id and version id
+ ...(operation !== SecretOperations.Create
+ ? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
+ : {})
+ };
+ });
+ const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(commits, tx);
+
+ return { ...approvalRequestDoc, commits: approvalCommits };
+ });
+ } else {
+ await secretDAL.transaction(async (tx) => {
+ if (locallyCreatedSecrets.length) {
+ await fnSecretBulkInsert({
+ folderId: destinationReplicationFolderId,
+ secretVersionDAL,
+ secretDAL,
+ tx,
+ secretTagDAL,
+ secretVersionTagDAL,
+ inputSecrets: locallyCreatedSecrets.map((doc) => {
+ return {
+ keyEncoding: doc.keyEncoding,
+ algorithm: doc.algorithm,
+ type: doc.type,
+ metadata: doc.metadata,
+ secretKeyIV: doc.secretKeyIV,
+ secretKeyTag: doc.secretKeyTag,
+ secretKeyCiphertext: doc.secretKeyCiphertext,
+ secretValueIV: doc.secretValueIV,
+ secretValueTag: doc.secretValueTag,
+ secretValueCiphertext: doc.secretValueCiphertext,
+ secretBlindIndex: doc.secretBlindIndex,
+ secretCommentIV: doc.secretCommentIV,
+ secretCommentTag: doc.secretCommentTag,
+ secretCommentCiphertext: doc.secretCommentCiphertext,
+ skipMultilineEncoding: doc.skipMultilineEncoding
+ };
+ })
+ });
+ }
+ if (locallyUpdatedSecrets.length) {
+ await fnSecretBulkUpdate({
+ projectId,
+ folderId: destinationReplicationFolderId,
+ secretVersionDAL,
+ secretDAL,
+ tx,
+ secretTagDAL,
+ secretVersionTagDAL,
+ inputSecrets: locallyUpdatedSecrets.map((doc) => {
+ return {
+ filter: {
+ folderId: destinationReplicationFolderId,
+ id: destinationLocalSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id
+ },
+ data: {
+ keyEncoding: doc.keyEncoding,
+ algorithm: doc.algorithm,
+ type: doc.type,
+ metadata: doc.metadata,
+ secretKeyIV: doc.secretKeyIV,
+ secretKeyTag: doc.secretKeyTag,
+ secretKeyCiphertext: doc.secretKeyCiphertext,
+ secretValueIV: doc.secretValueIV,
+ secretValueTag: doc.secretValueTag,
+ secretValueCiphertext: doc.secretValueCiphertext,
+ secretBlindIndex: doc.secretBlindIndex,
+ secretCommentIV: doc.secretCommentIV,
+ secretCommentTag: doc.secretCommentTag,
+ secretCommentCiphertext: doc.secretCommentCiphertext,
+ skipMultilineEncoding: doc.skipMultilineEncoding
+ }
+ };
+ })
+ });
+ }
+ if (locallyDeletedSecrets.length) {
+ await secretDAL.delete(
+ {
+ $in: {
+ id: locallyDeletedSecrets.map(({ id }) => id)
+ },
+ folderId: destinationReplicationFolderId
+ },
+ tx
+ );
+ }
+ });
+
+ await secretQueueService.syncSecrets({
+ projectId,
+ secretPath: destinationFolder.path,
+ environmentSlug: destinationFolder.environmentSlug,
+ actorId,
+ actor,
+ _depth: depth + 1,
+ _deDupeReplicationQueue: deDupeReplicationQueue,
+ _deDupeQueue: deDupeQueue
+ });
+ }
+
+ // this is used to avoid multiple times generating secret approval by failed one
+ await keyStore.setItemWithExpiry(
+ keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
+ SECRET_IMPORT_SUCCESS_LOCK,
+ 1,
+ KeyStorePrefixes.SecretReplication
+ );
+
+ await secretImportDAL.updateById(destinationSecretImport.id, {
+ lastReplicated: new Date(),
+ replicationStatus: null,
+ isReplicationSuccess: true
+ });
+ } catch (err) {
+ logger.error(
+ err,
+ `Failed to replicate secret with import id=[${destinationSecretImport.id}] env=[${destinationSecretImport.importEnv.slug}] path=[${destinationSecretImport.importPath}]`
+ );
+ await secretImportDAL.updateById(destinationSecretImport.id, {
+ lastReplicated: new Date(),
+ replicationStatus: (err as Error)?.message.slice(0, 500),
+ isReplicationSuccess: false
+ });
+ }
+ }
+ /* eslint-enable no-await-in-loop */
+ } finally {
+ await lock.release();
+ logger.info(job.data, "Replication finished");
+ }
+ });
+
+ queueService.listen(QueueName.SecretReplication, "failed", (job, err) => {
+ logger.error(err, "Failed to replicate secret", job?.data);
+ });
+};
diff --git a/backend/src/ee/services/secret-replication/secret-replication-types.ts b/backend/src/ee/services/secret-replication/secret-replication-types.ts
new file mode 100644
index 000000000..1b32f1f4a
--- /dev/null
+++ b/backend/src/ee/services/secret-replication/secret-replication-types.ts
@@ -0,0 +1,3 @@
+export type TSyncSecretReplicationDTO = {
+ id: string;
+};
diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
index 0e71ad126..3e1142969 100644
--- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
+++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
@@ -81,8 +81,7 @@ export const secretSnapshotServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
- const count = await snapshotDAL.countOfSnapshotsByFolderId(folder.id);
- return count;
+ return snapshotDAL.countOfSnapshotsByFolderId(folder.id);
};
const listSnapshots = async ({
@@ -220,7 +219,7 @@ export const secretSnapshotServiceFactory = ({
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
// this will remove all secrets and folders on child
// due to sql foreign key and link list connection removing the folders removes everything below too
- const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId }, tx);
+ const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId, isReserved: false }, tx);
const deletedTopLevelFolders = groupBy(
deletedFolders.filter(({ parentId }) => parentId === snapshot.folderId),
(item) => item.id
diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts
index cdd5a999b..4092bf356 100644
--- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts
+++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-await-in-loop */
import { Knex } from "knex";
import { TDbClient } from "@app/db";
@@ -11,6 +12,7 @@ import {
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
+import { logger } from "@app/lib/logger";
export type TSnapshotDALFactory = ReturnType;
@@ -325,12 +327,151 @@ export const snapshotDALFactory = (db: TDbClient) => {
}
};
+ /**
+ * Prunes excess snapshots from the database to ensure only a specified number of recent snapshots are retained for each folder.
+ *
+ * This function operates in three main steps:
+ * 1. Pruning snapshots from current folders.
+ * 2. Pruning snapshots from non-current folders (versioned ones).
+ * 3. Removing orphaned snapshots that do not belong to any existing folder or folder version.
+ *
+ * The function processes snapshots in batches, determined by the `PRUNE_FOLDER_BATCH_SIZE` constant,
+ * to manage the large datasets without overwhelming the DB.
+ *
+ * Steps:
+ * - Fetch a batch of folder IDs.
+ * - For each batch, use a Common Table Expression (CTE) to rank snapshots within each folder by their creation date.
+ * - Identify and delete snapshots that exceed the project's point-in-time version limit (`pitVersionLimit`).
+ * - Repeat the process for versioned folders.
+ * - Finally, delete orphaned snapshots that do not have an associated folder.
+ */
+ const pruneExcessSnapshots = async () => {
+ const PRUNE_FOLDER_BATCH_SIZE = 10000;
+
+ try {
+ let uuidOffset = "00000000-0000-0000-0000-000000000000";
+ // cleanup snapshots from current folders
+ // eslint-disable-next-line no-constant-condition, no-unreachable-loop
+ while (true) {
+ const folderBatch = await db(TableName.SecretFolder)
+ .where("id", ">", uuidOffset)
+ .where("isReserved", false)
+ .orderBy("id", "asc")
+ .limit(PRUNE_FOLDER_BATCH_SIZE)
+ .select("id");
+
+ const batchEntries = folderBatch.map((folder) => folder.id);
+
+ if (folderBatch.length) {
+ try {
+ logger.info(`Pruning snapshots in [range=${batchEntries[0]}:${batchEntries[batchEntries.length - 1]}]`);
+ await db(TableName.Snapshot)
+ .with("snapshot_cte", (qb) => {
+ void qb
+ .from(TableName.Snapshot)
+ .whereIn(`${TableName.Snapshot}.folderId`, batchEntries)
+ .select(
+ "folderId",
+ `${TableName.Snapshot}.id as id`,
+ db.raw(
+ `ROW_NUMBER() OVER (PARTITION BY ${TableName.Snapshot}."folderId" ORDER BY ${TableName.Snapshot}."createdAt" DESC) AS row_num`
+ )
+ );
+ })
+ .join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.Snapshot}.folderId`)
+ .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
+ .join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
+ .join("snapshot_cte", "snapshot_cte.id", `${TableName.Snapshot}.id`)
+ .whereRaw(`snapshot_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
+ .delete();
+ } catch (err) {
+ logger.error(
+ `Failed to prune snapshots from current folders in range ${batchEntries[0]}:${
+ batchEntries[batchEntries.length - 1]
+ }`
+ );
+ } finally {
+ uuidOffset = batchEntries[batchEntries.length - 1];
+ }
+ } else {
+ break;
+ }
+ }
+
+ // cleanup snapshots from non-current folders
+ uuidOffset = "00000000-0000-0000-0000-000000000000";
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const folderBatch = await db(TableName.SecretFolderVersion)
+ .select("folderId")
+ .distinct("folderId")
+ .where("folderId", ">", uuidOffset)
+ .orderBy("folderId", "asc")
+ .limit(PRUNE_FOLDER_BATCH_SIZE);
+
+ const batchEntries = folderBatch.map((folder) => folder.folderId);
+
+ if (folderBatch.length) {
+ try {
+ logger.info(`Pruning snapshots in range ${batchEntries[0]}:${batchEntries[batchEntries.length - 1]}`);
+ await db(TableName.Snapshot)
+ .with("snapshot_cte", (qb) => {
+ void qb
+ .from(TableName.Snapshot)
+ .whereIn(`${TableName.Snapshot}.folderId`, batchEntries)
+ .select(
+ "folderId",
+ `${TableName.Snapshot}.id as id`,
+ db.raw(
+ `ROW_NUMBER() OVER (PARTITION BY ${TableName.Snapshot}."folderId" ORDER BY ${TableName.Snapshot}."createdAt" DESC) AS row_num`
+ )
+ );
+ })
+ .join(
+ TableName.SecretFolderVersion,
+ `${TableName.SecretFolderVersion}.folderId`,
+ `${TableName.Snapshot}.folderId`
+ )
+ .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolderVersion}.envId`)
+ .join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
+ .join("snapshot_cte", "snapshot_cte.id", `${TableName.Snapshot}.id`)
+ .whereRaw(`snapshot_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
+ .delete();
+ } catch (err) {
+ logger.error(
+ `Failed to prune snapshots from non-current folders in range ${batchEntries[0]}:${
+ batchEntries[batchEntries.length - 1]
+ }`
+ );
+ } finally {
+ uuidOffset = batchEntries[batchEntries.length - 1];
+ }
+ } else {
+ break;
+ }
+ }
+
+ // cleanup orphaned snapshots (those that don't belong to an existing folder and folder version)
+ await db(TableName.Snapshot)
+ .whereNotIn("folderId", (qb) => {
+ void qb
+ .select("folderId")
+ .from(TableName.SecretFolderVersion)
+ .union((qb1) => void qb1.select("id").from(TableName.SecretFolder));
+ })
+ .delete();
+ } catch (error) {
+ throw new DatabaseError({ error, name: "SnapshotPrune" });
+ }
+ };
+
return {
...secretSnapshotOrm,
findById,
findLatestSnapshotByFolderId,
findRecursivelySnapshots,
countOfSnapshotsByFolderId,
- findSecretSnapshotDataById
+ findSecretSnapshotDataById,
+ pruneExcessSnapshots
};
};
diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts
index 5e2c3aab3..ce752a1e5 100644
--- a/backend/src/keystore/keystore.ts
+++ b/backend/src/keystore/keystore.ts
@@ -1,20 +1,75 @@
import { Redis } from "ioredis";
+import { Redlock, Settings } from "@app/lib/red-lock";
+
export type TKeyStoreFactory = ReturnType;
+// all the key prefixes used must be set here to avoid conflict
+export enum KeyStorePrefixes {
+ SecretReplication = "secret-replication-import-lock"
+}
+
+type TWaitTillReady = {
+ key: string;
+ waitingCb?: () => void;
+ keyCheckCb: (val: string | null) => boolean;
+ waitIteration?: number;
+ delay?: number;
+ jitter?: number;
+};
+
export const keyStoreFactory = (redisUrl: string) => {
const redis = new Redis(redisUrl);
+ const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
- const setItem = async (key: string, value: string | number | Buffer) => redis.set(key, value);
+ const setItem = async (key: string, value: string | number | Buffer, prefix?: string) =>
+ redis.set(prefix ? `${prefix}:${key}` : key, value);
- const getItem = async (key: string) => redis.get(key);
+ const getItem = async (key: string, prefix?: string) => redis.get(prefix ? `${prefix}:${key}` : key);
- const setItemWithExpiry = async (key: string, exp: number | string, value: string | number | Buffer) =>
- redis.setex(key, exp, value);
+ const setItemWithExpiry = async (
+ key: string,
+ exp: number | string,
+ value: string | number | Buffer,
+ prefix?: string
+ ) => redis.setex(prefix ? `${prefix}:${key}` : key, exp, value);
const deleteItem = async (key: string) => redis.del(key);
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
- return { setItem, getItem, setItemWithExpiry, deleteItem, incrementBy };
+ const waitTillReady = async ({
+ key,
+ waitingCb,
+ keyCheckCb,
+ waitIteration = 10,
+ delay = 1000,
+ jitter = 200
+ }: TWaitTillReady) => {
+ let attempts = 0;
+ let isReady = keyCheckCb(await getItem(key));
+ while (!isReady) {
+ if (attempts > waitIteration) return;
+ // eslint-disable-next-line
+ await new Promise((resolve) => {
+ waitingCb?.();
+ setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
+ });
+ attempts += 1;
+ // eslint-disable-next-line
+ isReady = keyCheckCb(await getItem(key, "wait_till_ready"));
+ }
+ };
+
+ return {
+ setItem,
+ getItem,
+ setItemWithExpiry,
+ deleteItem,
+ incrementBy,
+ acquireLock(resources: string[], duration: number, settings?: Partial) {
+ return redisLock.acquire(resources, duration, settings);
+ },
+ waitTillReady
+ };
};
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 6ae5a9d33..d768066fa 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -225,7 +225,8 @@ export const PROJECT_IDENTITIES = {
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
- isTemporary: "Whether the assigned role is temporary.",
+ isTemporary:
+ "Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
temporaryAccessStartTime: "Time to which the temporary access starts"
@@ -242,7 +243,8 @@ export const PROJECT_IDENTITIES = {
roles: {
description: "A list of role slugs to assign to the newly created identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
- isTemporary: "Whether the assigned role is temporary.",
+ isTemporary:
+ "Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
temporaryAccessStartTime: "Time to which the temporary access starts"
@@ -341,7 +343,8 @@ export const RAW_SECRETS = {
secretValue: "The value of the secret to create.",
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to create.",
- workspaceId: "The ID of the project to create the secret in."
+ workspaceId: "The ID of the project to create the secret in.",
+ tagIds: "The ID of the tags to be attached to the created secret."
},
GET: {
secretName: "The name of the secret to get.",
@@ -362,7 +365,8 @@ export const RAW_SECRETS = {
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to update.",
projectSlug: "The slug of the project to update the secret in.",
- workspaceId: "The ID of the project to update the secret in."
+ workspaceId: "The ID of the project to update the secret in.",
+ tagIds: "The ID of the tags to be attached to the updated secret."
},
DELETE: {
secretName: "The name of the secret to delete.",
@@ -384,6 +388,8 @@ export const SECRET_IMPORTS = {
environment: "The slug of the environment to import into.",
path: "The path to import into.",
workspaceId: "The ID of the project you are working in.",
+ isReplication:
+ "When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.",
import: {
environment: "The slug of the environment to import from.",
path: "The path to import from."
@@ -502,12 +508,27 @@ export const SECRET_TAGS = {
LIST: {
projectId: "The ID of the project to list tags from."
},
+ GET_TAG_BY_ID: {
+ projectId: "The ID of the project to get tags from.",
+ tagId: "The ID of the tag to get details"
+ },
+ GET_TAG_BY_SLUG: {
+ projectId: "The ID of the project to get tags from.",
+ tagSlug: "The slug of the tag to get details"
+ },
CREATE: {
projectId: "The ID of the project to create the tag in.",
name: "The name of the tag to create.",
slug: "The slug of the tag to create.",
color: "The color of the tag to create."
},
+ UPDATE: {
+ projectId: "The ID of the project to update the tag in.",
+ tagId: "The ID of the tag to get details",
+ name: "The name of the tag to update.",
+ slug: "The slug of the tag to update.",
+ color: "The color of the tag to update."
+ },
DELETE: {
tagId: "The ID of the tag to delete.",
projectId: "The ID of the project to delete the tag from."
@@ -519,7 +540,8 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to create.",
slug: "The slug of the privilege to create.",
- permissions: `The permission object for the privilege.
+ permissions: `@deprecated - use privilegePermission
+The permission object for the privilege.
- Read secrets
\`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"]}
@@ -533,6 +555,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
\`\`\`
`,
+ privilegePermission: "The permission object for the privilege.",
isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
@@ -544,7 +567,8 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
identityId: "The ID of the identity to update.",
slug: "The slug of the privilege to update.",
newSlug: "The new slug of the privilege to update.",
- permissions: `The permission object for the privilege.
+ permissions: `@deprecated - use privilegePermission
+The permission object for the privilege.
- Read secrets
\`\`\`
{ "permissions": [{"action": "read", "subject": "secrets"]}
@@ -558,6 +582,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
\`\`\`
`,
+ privilegePermission: "The permission object for the privilege.",
isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
@@ -655,6 +680,7 @@ export const INTEGRATION = {
targetServiceId:
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
owner: "External integration providers service entity owner. Used in Github.",
+ url: "The self-hosted URL of the platform to integrate with",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
region: "AWS region to sync secrets to.",
scope: "Scope of the provider. Used by Github, Qovery",
@@ -667,7 +693,10 @@ export const INTEGRATION = {
secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.",
kmsKeyId: "The ID of the encryption key from AWS KMS.",
- shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store."
+ shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
+ shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
+ shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
+ shouldEnableDelete: "The flag to enable deletion of secrets"
}
},
UPDATE: {
@@ -715,3 +744,128 @@ export const AUDIT_LOG_STREAMS = {
id: "The ID of the audit log stream to get details."
}
};
+
+export const CERTIFICATE_AUTHORITIES = {
+ CREATE: {
+ projectSlug: "Slug of the project to create the CA in.",
+ type: "The type of CA to create",
+ friendlyName: "A friendly name for the CA",
+ organization: "The organization (O) for the CA",
+ ou: "The organization unit (OU) for the CA",
+ country: "The country name (C) for the CA",
+ province: "The state of province name for the CA",
+ locality: "The locality name for the CA",
+ commonName: "The common name (CN) for the CA",
+ notBefore: "The date and time when the CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
+ notAfter: "The date and time when the CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
+ maxPathLength:
+ "The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
+ keyAlgorithm:
+ "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA."
+ },
+ GET: {
+ caId: "The ID of the CA to get"
+ },
+ UPDATE: {
+ caId: "The ID of the CA to update",
+ status: "The status of the CA to update to. This can be one of active or disabled"
+ },
+ DELETE: {
+ caId: "The ID of the CA to delete"
+ },
+ GET_CSR: {
+ caId: "The ID of the CA to generate CSR from",
+ csr: "The generated CSR from the CA"
+ },
+ GET_CERT: {
+ caId: "The ID of the CA to get the certificate body and certificate chain from",
+ certificate: "The certificate body of the CA",
+ certificateChain: "The certificate chain of the CA",
+ serialNumber: "The serial number of the CA certificate"
+ },
+ SIGN_INTERMEDIATE: {
+ caId: "The ID of the CA to sign the intermediate certificate with",
+ csr: "The CSR to sign with the CA",
+ notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
+ notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
+ maxPathLength:
+ "The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
+ certificate: "The signed intermediate certificate",
+ certificateChain: "The certificate chain of the intermediate certificate",
+ issuingCaCertificate: "The certificate of the issuing CA",
+ serialNumber: "The serial number of the intermediate certificate"
+ },
+ IMPORT_CERT: {
+ caId: "The ID of the CA to import the certificate for",
+ certificate: "The certificate body to import",
+ certificateChain: "The certificate chain to import"
+ },
+ ISSUE_CERT: {
+ caId: "The ID of the CA to issue the certificate from",
+ friendlyName: "A friendly name for the certificate",
+ commonName: "The common name (CN) for the certificate",
+ ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
+ notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
+ notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
+ certificate: "The issued certificate",
+ issuingCaCertificate: "The certificate of the issuing CA",
+ certificateChain: "The certificate chain of the issued certificate",
+ privateKey: "The private key of the issued certificate",
+ serialNumber: "The serial number of the issued certificate"
+ },
+ GET_CRL: {
+ caId: "The ID of the CA to get the certificate revocation list (CRL) for",
+ crl: "The certificate revocation list (CRL) of the CA"
+ }
+};
+
+export const CERTIFICATES = {
+ GET: {
+ serialNumber: "The serial number of the certificate to get"
+ },
+ REVOKE: {
+ serialNumber:
+ "The serial number of the certificate to revoke. The revoked certificate will be added to the certificate revocation list (CRL) of the CA.",
+ revocationReason: "The reason for revoking the certificate.",
+ revokedAt: "The date and time when the certificate was revoked",
+ serialNumberRes: "The serial number of the revoked certificate."
+ },
+ DELETE: {
+ serialNumber: "The serial number of the certificate to delete"
+ },
+ GET_CERT: {
+ serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for",
+ certificate: "The certificate body of the certificate",
+ certificateChain: "The certificate chain of the certificate",
+ serialNumberRes: "The serial number of the certificate"
+ }
+};
+
+export const PROJECT_ROLE = {
+ CREATE: {
+ projectSlug: "Slug of the project to create the role for.",
+ slug: "The slug of the role.",
+ name: "The name of the role.",
+ description: "The description for the role.",
+ permissions: "The permissions assigned to the role."
+ },
+ UPDATE: {
+ projectSlug: "Slug of the project to update the role for.",
+ roleId: "The ID of the role to update",
+ slug: "The slug of the role.",
+ name: "The name of the role.",
+ description: "The description for the role.",
+ permissions: "The permissions assigned to the role."
+ },
+ DELETE: {
+ projectSlug: "Slug of the project to delete this role for.",
+ roleId: "The ID of the role to update"
+ },
+ GET_ROLE_BY_SLUG: {
+ projectSlug: "The slug of the project.",
+ roleSlug: "The slug of the role to get details"
+ },
+ LIST: {
+ projectSlug: "The slug of the project to list the roles of."
+ }
+};
diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts
index 6ae8bf02d..f4da71293 100644
--- a/backend/src/lib/config/env.ts
+++ b/backend/src/lib/config/env.ts
@@ -29,7 +29,7 @@ const envSchema = z
DB_USER: zpStr(z.string().describe("Postgres database username").optional()),
DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()),
DB_NAME: zpStr(z.string().describe("Postgres database name").optional()),
-
+ BCRYPT_SALT_ROUND: z.number().default(12),
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
SALT_ROUNDS: z.coerce.number().default(10),
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
@@ -39,7 +39,9 @@ const envSchema = z
HTTPS_ENABLED: zodStrBool,
// smtp options
SMTP_HOST: zpStr(z.string().optional()),
- SMTP_SECURE: zodStrBool,
+ SMTP_IGNORE_TLS: zodStrBool.default("false"),
+ SMTP_REQUIRE_TLS: zodStrBool.default("true"),
+ SMTP_TLS_REJECT_UNAUTHORIZED: zodStrBool.default("true"),
SMTP_PORT: z.coerce.number().default(587),
SMTP_USERNAME: zpStr(z.string().optional()),
SMTP_PASSWORD: zpStr(z.string().optional()),
@@ -75,6 +77,7 @@ const envSchema = z
.optional()
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
), // fallback since URL_GITLAB_LOGIN has been renamed
+ DEFAULT_SAML_ORG_SLUG: zpStr(z.string().optional()).default(process.env.NEXT_PUBLIC_SAML_ORG_SLUG),
// integration client secrets
// heroku
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
@@ -119,7 +122,8 @@ const envSchema = z
.transform((val) => val === "true")
.optional(),
INFISICAL_CLOUD: zodStrBool.default("false"),
- MAINTENANCE_MODE: zodStrBool.default("false")
+ MAINTENANCE_MODE: zodStrBool.default("false"),
+ CAPTCHA_SECRET: zpStr(z.string().optional())
})
.transform((data) => ({
...data,
@@ -131,7 +135,8 @@ const envSchema = z
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
- Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET)
+ Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
+ samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
}));
let envCfg: Readonly>;
@@ -150,13 +155,20 @@ export const initEnvConfig = (logger: Logger) => {
return envCfg;
};
-export const formatSmtpConfig = () => ({
- host: envCfg.SMTP_HOST,
- port: envCfg.SMTP_PORT,
- auth:
- envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
- ? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
- : undefined,
- secure: envCfg.SMTP_SECURE,
- from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`
-});
+export const formatSmtpConfig = () => {
+ return {
+ host: envCfg.SMTP_HOST,
+ port: envCfg.SMTP_PORT,
+ auth:
+ envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
+ ? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
+ : undefined,
+ secure: envCfg.SMTP_PORT === 465,
+ from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`,
+ ignoreTLS: envCfg.SMTP_IGNORE_TLS,
+ requireTLS: envCfg.SMTP_REQUIRE_TLS,
+ tls: {
+ rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED
+ }
+ };
+};
diff --git a/backend/src/lib/crypto/cipher/cipher.ts b/backend/src/lib/crypto/cipher/cipher.ts
new file mode 100644
index 000000000..7bc16b470
--- /dev/null
+++ b/backend/src/lib/crypto/cipher/cipher.ts
@@ -0,0 +1,49 @@
+import crypto from "crypto";
+
+import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
+
+const getIvLength = () => {
+ return 12;
+};
+
+const getTagLength = () => {
+ return 16;
+};
+
+export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
+ const IV_LENGTH = getIvLength();
+ const TAG_LENGTH = getTagLength();
+
+ const encrypt = (text: Buffer, key: Buffer) => {
+ const iv = crypto.randomBytes(IV_LENGTH);
+ const cipher = crypto.createCipheriv(type, key, iv);
+
+ let encrypted = cipher.update(text);
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
+
+ // Get the authentication tag
+ const tag = cipher.getAuthTag();
+
+ // Concatenate IV, encrypted text, and tag into a single buffer
+ const ciphertextBlob = Buffer.concat([iv, encrypted, tag]);
+ return ciphertextBlob;
+ };
+
+ const decrypt = (ciphertextBlob: Buffer, key: Buffer) => {
+ // Extract the IV, encrypted text, and tag from the buffer
+ const iv = ciphertextBlob.subarray(0, IV_LENGTH);
+ const tag = ciphertextBlob.subarray(-TAG_LENGTH);
+ const encrypted = ciphertextBlob.subarray(IV_LENGTH, -TAG_LENGTH);
+
+ const decipher = crypto.createDecipheriv(type, key, iv);
+ decipher.setAuthTag(tag);
+
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
+ return decrypted;
+ };
+
+ return {
+ encrypt,
+ decrypt
+ };
+};
diff --git a/backend/src/lib/crypto/cipher/index.ts b/backend/src/lib/crypto/cipher/index.ts
new file mode 100644
index 000000000..41dbcf639
--- /dev/null
+++ b/backend/src/lib/crypto/cipher/index.ts
@@ -0,0 +1,2 @@
+export { symmetricCipherService } from "./cipher";
+export { SymmetricEncryption } from "./types";
diff --git a/backend/src/lib/crypto/cipher/types.ts b/backend/src/lib/crypto/cipher/types.ts
new file mode 100644
index 000000000..f490d6a66
--- /dev/null
+++ b/backend/src/lib/crypto/cipher/types.ts
@@ -0,0 +1,9 @@
+export enum SymmetricEncryption {
+ AES_GCM_256 = "aes-256-gcm",
+ AES_GCM_128 = "aes-128-gcm"
+}
+
+export type TSymmetricEncryptionFns = {
+ encrypt: (text: Buffer, key: Buffer) => Buffer;
+ decrypt: (blob: Buffer, key: Buffer) => Buffer;
+};
diff --git a/backend/src/lib/crypto/encryption.ts b/backend/src/lib/crypto/encryption.ts
index 16a7f42e7..6af20862b 100644
--- a/backend/src/lib/crypto/encryption.ts
+++ b/backend/src/lib/crypto/encryption.ts
@@ -11,6 +11,8 @@ import { getConfig } from "../config/env";
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
+export const randomSecureBytes = (length = 32) => crypto.randomBytes(length);
+
export type TDecryptSymmetricInput = {
ciphertext: string;
iv: string;
diff --git a/backend/src/lib/crypto/index.ts b/backend/src/lib/crypto/index.ts
index db3d91fc8..cc6acfb80 100644
--- a/backend/src/lib/crypto/index.ts
+++ b/backend/src/lib/crypto/index.ts
@@ -9,7 +9,8 @@ export {
encryptAsymmetric,
encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8,
- generateAsymmetricKeyPair
+ generateAsymmetricKeyPair,
+ randomSecureBytes
} from "./encryption";
export {
decryptIntegrationAuths,
diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts
index bc29cdb3f..8d7ea656a 100644
--- a/backend/src/lib/crypto/srp.ts
+++ b/backend/src/lib/crypto/srp.ts
@@ -6,7 +6,7 @@ import tweetnacl from "tweetnacl-util";
import { TUserEncryptionKeys } from "@app/db/schemas";
-import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
+import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
export const generateSrpServerKey = async (salt: string, verifier: string) => {
// eslint-disable-next-line new-cap
@@ -97,7 +97,13 @@ export const generateUserSrpKeys = async (email: string, password: string) => {
};
};
-export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
+export const getUserPrivateKey = async (
+ password: string,
+ user: Pick<
+ TUserEncryptionKeys,
+ "protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag"
+ >
+) => {
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
@@ -108,17 +114,18 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
raw: true
});
if (!derivedKey) throw new Error("Failed to derive key from password");
- const key = decryptSymmetric({
- ciphertext: user.protectedKey!,
- iv: user.protectedKeyIV!,
- tag: user.protectedKeyTag!,
- key: derivedKey.toString("base64")
+ const key = decryptSymmetric128BitHexKeyUTF8({
+ ciphertext: user.protectedKey as string,
+ iv: user.protectedKeyIV as string,
+ tag: user.protectedKeyTag as string,
+ key: derivedKey
});
- const privateKey = decryptSymmetric({
+
+ const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
- key
+ key: Buffer.from(key, "hex")
});
return privateKey;
};
diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts
index 18b40acfd..0a7cb8014 100644
--- a/backend/src/lib/errors/index.ts
+++ b/backend/src/lib/errors/index.ts
@@ -59,6 +59,18 @@ export class BadRequestError extends Error {
}
}
+export class NotFoundError extends Error {
+ name: string;
+
+ error: unknown;
+
+ constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
+ super(message ?? "The requested entity is not found");
+ this.name = name || "NotFound";
+ this.error = error;
+ }
+}
+
export class DisableRotationErrors extends Error {
name: string;
diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts
index d78020809..0faeba290 100644
--- a/backend/src/lib/knex/index.ts
+++ b/backend/src/lib/knex/index.ts
@@ -104,24 +104,68 @@ export const ormify = (db: Kne
throw new DatabaseError({ error, name: "Create" });
}
},
- updateById: async (id: string, data: Tables[Tname]["update"], tx?: Knex) => {
+ updateById: async (
+ id: string,
+ {
+ $incr,
+ $decr,
+ ...data
+ }: Tables[Tname]["update"] & {
+ $incr?: { [x in keyof Partial]: number };
+ $decr?: { [x in keyof Partial]: number };
+ },
+ tx?: Knex
+ ) => {
try {
- const [res] = await (tx || db)(tableName)
+ const query = (tx || db)(tableName)
.where({ id } as never)
.update(data as never)
.returning("*");
- return res;
+ if ($incr) {
+ Object.entries($incr).forEach(([incrementField, incrementValue]) => {
+ void query.increment(incrementField, incrementValue);
+ });
+ }
+ if ($decr) {
+ Object.entries($decr).forEach(([incrementField, incrementValue]) => {
+ void query.decrement(incrementField, incrementValue);
+ });
+ }
+ const [docs] = await query;
+ return docs;
} catch (error) {
throw new DatabaseError({ error, name: "Update by id" });
}
},
- update: async (filter: TFindFilter, data: Tables[Tname]["update"], tx?: Knex) => {
+ update: async (
+ filter: TFindFilter,
+ {
+ $incr,
+ $decr,
+ ...data
+ }: Tables[Tname]["update"] & {
+ $incr?: { [x in keyof Partial]: number };
+ $decr?: { [x in keyof Partial]: number };
+ },
+ tx?: Knex
+ ) => {
try {
- const res = await (tx || db)(tableName)
+ const query = (tx || db)(tableName)
.where(buildFindFilter(filter))
.update(data as never)
.returning("*");
- return res;
+ // increment and decrement operation in update
+ if ($incr) {
+ Object.entries($incr).forEach(([incrementField, incrementValue]) => {
+ void query.increment(incrementField, incrementValue);
+ });
+ }
+ if ($decr) {
+ Object.entries($decr).forEach(([incrementField, incrementValue]) => {
+ void query.increment(incrementField, incrementValue);
+ });
+ }
+ return await query;
} catch (error) {
throw new DatabaseError({ error, name: "Update" });
}
diff --git a/backend/src/lib/red-lock/index.ts b/backend/src/lib/red-lock/index.ts
new file mode 100644
index 000000000..e1cc4f587
--- /dev/null
+++ b/backend/src/lib/red-lock/index.ts
@@ -0,0 +1,682 @@
+/* eslint-disable */
+// Source code credits: https://github.com/mike-marcacci/node-redlock
+// Taken to avoid external dependency
+import { randomBytes, createHash } from "crypto";
+import { EventEmitter } from "events";
+
+// AbortController became available as a global in node version 16. Once version
+// 14 reaches its end-of-life, this can be removed.
+
+import { Redis as IORedisClient, Cluster as IORedisCluster } from "ioredis";
+
+type Client = IORedisClient | IORedisCluster;
+
+// Define script constants.
+const ACQUIRE_SCRIPT = `
+ -- Return 0 if an entry already exists.
+ for i, key in ipairs(KEYS) do
+ if redis.call("exists", key) == 1 then
+ return 0
+ end
+ end
+
+ -- Create an entry for each provided key.
+ for i, key in ipairs(KEYS) do
+ redis.call("set", key, ARGV[1], "PX", ARGV[2])
+ end
+
+ -- Return the number of entries added.
+ return #KEYS
+`;
+
+const EXTEND_SCRIPT = `
+ -- Return 0 if an entry exists with a *different* lock value.
+ for i, key in ipairs(KEYS) do
+ if redis.call("get", key) ~= ARGV[1] then
+ return 0
+ end
+ end
+
+ -- Update the entry for each provided key.
+ for i, key in ipairs(KEYS) do
+ redis.call("set", key, ARGV[1], "PX", ARGV[2])
+ end
+
+ -- Return the number of entries updated.
+ return #KEYS
+`;
+
+const RELEASE_SCRIPT = `
+ local count = 0
+ for i, key in ipairs(KEYS) do
+ -- Only remove entries for *this* lock value.
+ if redis.call("get", key) == ARGV[1] then
+ redis.pcall("del", key)
+ count = count + 1
+ end
+ end
+
+ -- Return the number of entries removed.
+ return count
+`;
+
+export type ClientExecutionResult =
+ | {
+ client: Client;
+ vote: "for";
+ value: number;
+ }
+ | {
+ client: Client;
+ vote: "against";
+ error: Error;
+ };
+
+/*
+ * This object contains a summary of results.
+ */
+export type ExecutionStats = {
+ readonly membershipSize: number;
+ readonly quorumSize: number;
+ readonly votesFor: Set;
+ readonly votesAgainst: Map;
+};
+
+/*
+ * This object contains a summary of results. Because the result of an attempt
+ * can sometimes be determined before all requests are finished, each attempt
+ * contains a Promise that will resolve ExecutionStats once all requests are
+ * finished. A rejection of these promises should be considered undefined
+ * behavior and should cause a crash.
+ */
+export type ExecutionResult = {
+ attempts: ReadonlyArray>;
+ start: number;
+};
+
+/**
+ *
+ */
+export interface Settings {
+ readonly driftFactor: number;
+ readonly retryCount: number;
+ readonly retryDelay: number;
+ readonly retryJitter: number;
+ readonly automaticExtensionThreshold: number;
+}
+
+// Define default settings.
+const defaultSettings: Readonly = {
+ driftFactor: 0.01,
+ retryCount: 10,
+ retryDelay: 200,
+ retryJitter: 100,
+ automaticExtensionThreshold: 500
+};
+
+// Modifyng this object is forbidden.
+Object.freeze(defaultSettings);
+
+/*
+ * This error indicates a failure due to the existence of another lock for one
+ * or more of the requested resources.
+ */
+export class ResourceLockedError extends Error {
+ constructor(public readonly message: string) {
+ super();
+ this.name = "ResourceLockedError";
+ }
+}
+
+/*
+ * This error indicates a failure of an operation to pass with a quorum.
+ */
+export class ExecutionError extends Error {
+ constructor(
+ public readonly message: string,
+ public readonly attempts: ReadonlyArray>
+ ) {
+ super();
+ this.name = "ExecutionError";
+ }
+}
+
+/*
+ * An object of this type is returned when a resource is successfully locked. It
+ * contains convenience methods `release` and `extend` which perform the
+ * associated Redlock method on itself.
+ */
+export class Lock {
+ constructor(
+ public readonly redlock: Redlock,
+ public readonly resources: string[],
+ public readonly value: string,
+ public readonly attempts: ReadonlyArray>,
+ public expiration: number
+ ) {}
+
+ async release(): Promise {
+ return this.redlock.release(this);
+ }
+
+ async extend(duration: number): Promise {
+ return this.redlock.extend(this, duration);
+ }
+}
+
+export type RedlockAbortSignal = AbortSignal & { error?: Error };
+
+/**
+ * A redlock object is instantiated with an array of at least one redis client
+ * and an optional `options` object. Properties of the Redlock object should NOT
+ * be changed after it is first used, as doing so could have unintended
+ * consequences for live locks.
+ */
+export class Redlock extends EventEmitter {
+ public readonly clients: Set;
+ public readonly settings: Settings;
+ public readonly scripts: {
+ readonly acquireScript: { value: string; hash: string };
+ readonly extendScript: { value: string; hash: string };
+ readonly releaseScript: { value: string; hash: string };
+ };
+
+ public constructor(
+ clients: Iterable,
+ settings: Partial = {},
+ scripts: {
+ readonly acquireScript?: string | ((script: string) => string);
+ readonly extendScript?: string | ((script: string) => string);
+ readonly releaseScript?: string | ((script: string) => string);
+ } = {}
+ ) {
+ super();
+
+ // Prevent crashes on error events.
+ this.on("error", () => {
+ // Because redlock is designed for high availability, it does not care if
+ // a minority of redis instances/clusters fail at an operation.
+ //
+ // However, it can be helpful to monitor and log such cases. Redlock emits
+ // an "error" event whenever it encounters an error, even if the error is
+ // ignored in its normal operation.
+ //
+ // This function serves to prevent node's default behavior of crashing
+ // when an "error" event is emitted in the absence of listeners.
+ });
+
+ // Create a new array of client, to ensure no accidental mutation.
+ this.clients = new Set(clients);
+ if (this.clients.size === 0) {
+ throw new Error("Redlock must be instantiated with at least one redis client.");
+ }
+
+ // Customize the settings for this instance.
+ this.settings = {
+ driftFactor: typeof settings.driftFactor === "number" ? settings.driftFactor : defaultSettings.driftFactor,
+ retryCount: typeof settings.retryCount === "number" ? settings.retryCount : defaultSettings.retryCount,
+ retryDelay: typeof settings.retryDelay === "number" ? settings.retryDelay : defaultSettings.retryDelay,
+ retryJitter: typeof settings.retryJitter === "number" ? settings.retryJitter : defaultSettings.retryJitter,
+ automaticExtensionThreshold:
+ typeof settings.automaticExtensionThreshold === "number"
+ ? settings.automaticExtensionThreshold
+ : defaultSettings.automaticExtensionThreshold
+ };
+
+ // Use custom scripts and script modifiers.
+ const acquireScript =
+ typeof scripts.acquireScript === "function" ? scripts.acquireScript(ACQUIRE_SCRIPT) : ACQUIRE_SCRIPT;
+ const extendScript =
+ typeof scripts.extendScript === "function" ? scripts.extendScript(EXTEND_SCRIPT) : EXTEND_SCRIPT;
+ const releaseScript =
+ typeof scripts.releaseScript === "function" ? scripts.releaseScript(RELEASE_SCRIPT) : RELEASE_SCRIPT;
+
+ this.scripts = {
+ acquireScript: {
+ value: acquireScript,
+ hash: this._hash(acquireScript)
+ },
+ extendScript: {
+ value: extendScript,
+ hash: this._hash(extendScript)
+ },
+ releaseScript: {
+ value: releaseScript,
+ hash: this._hash(releaseScript)
+ }
+ };
+ }
+
+ /**
+ * Generate a sha1 hash compatible with redis evalsha.
+ */
+ private _hash(value: string): string {
+ return createHash("sha1").update(value).digest("hex");
+ }
+
+ /**
+ * Generate a cryptographically random string.
+ */
+ private _random(): string {
+ return randomBytes(16).toString("hex");
+ }
+
+ /**
+ * This method runs `.quit()` on all client connections.
+ */
+ public async quit(): Promise {
+ const results = [];
+ for (const client of this.clients) {
+ results.push(client.quit());
+ }
+
+ await Promise.all(results);
+ }
+
+ /**
+ * This method acquires a locks on the resources for the duration specified by
+ * the `duration`.
+ */
+ public async acquire(resources: string[], duration: number, settings?: Partial): Promise {
+ if (Math.floor(duration) !== duration) {
+ throw new Error("Duration must be an integer value in milliseconds.");
+ }
+
+ const value = this._random();
+
+ try {
+ const { attempts, start } = await this._execute(
+ this.scripts.acquireScript,
+ resources,
+ [value, duration],
+ settings
+ );
+
+ // Add 2 milliseconds to the drift to account for Redis expires precision,
+ // which is 1 ms, plus the configured allowable drift factor.
+ const drift = Math.round((settings?.driftFactor ?? this.settings.driftFactor) * duration) + 2;
+
+ return new Lock(this, resources, value, attempts, start + duration - drift);
+ } catch (error) {
+ // If there was an error acquiring the lock, release any partial lock
+ // state that may exist on a minority of clients.
+ await this._execute(this.scripts.releaseScript, resources, [value], {
+ retryCount: 0
+ }).catch(() => {
+ // Any error here will be ignored.
+ });
+
+ throw error;
+ }
+ }
+
+ /**
+ * This method unlocks the provided lock from all servers still persisting it.
+ * It will fail with an error if it is unable to release the lock on a quorum
+ * of nodes, but will make no attempt to restore the lock in the case of a
+ * failure to release. It is safe to re-attempt a release or to ignore the
+ * error, as the lock will automatically expire after its timeout.
+ */
+ public async release(lock: Lock, settings?: Partial): Promise {
+ // Immediately invalidate the lock.
+ lock.expiration = 0;
+
+ // Attempt to release the lock.
+ return this._execute(this.scripts.releaseScript, lock.resources, [lock.value], settings);
+ }
+
+ /**
+ * This method extends a valid lock by the provided `duration`.
+ */
+ public async extend(existing: Lock, duration: number, settings?: Partial): Promise {
+ if (Math.floor(duration) !== duration) {
+ throw new Error("Duration must be an integer value in milliseconds.");
+ }
+
+ // The lock has already expired.
+ if (existing.expiration < Date.now()) {
+ throw new ExecutionError("Cannot extend an already-expired lock.", []);
+ }
+
+ const { attempts, start } = await this._execute(
+ this.scripts.extendScript,
+ existing.resources,
+ [existing.value, duration],
+ settings
+ );
+
+ // Invalidate the existing lock.
+ existing.expiration = 0;
+
+ // Add 2 milliseconds to the drift to account for Redis expires precision,
+ // which is 1 ms, plus the configured allowable drift factor.
+ const drift = Math.round((settings?.driftFactor ?? this.settings.driftFactor) * duration) + 2;
+
+ const replacement = new Lock(this, existing.resources, existing.value, attempts, start + duration - drift);
+
+ return replacement;
+ }
+
+ /**
+ * Execute a script on all clients. The resulting promise is resolved or
+ * rejected as soon as this quorum is reached; the resolution or rejection
+ * will contains a `stats` property that is resolved once all votes are in.
+ */
+ private async _execute(
+ script: { value: string; hash: string },
+ keys: string[],
+ args: (string | number)[],
+ _settings?: Partial
+ ): Promise {
+ const settings = _settings
+ ? {
+ ...this.settings,
+ ..._settings
+ }
+ : this.settings;
+
+ // For the purpose of easy config serialization, we treat a retryCount of
+ // -1 a equivalent to Infinity.
+ const maxAttempts = settings.retryCount === -1 ? Infinity : settings.retryCount + 1;
+
+ const attempts: Promise[] = [];
+
+ while (true) {
+ const { vote, stats, start } = await this._attemptOperation(script, keys, args);
+
+ attempts.push(stats);
+
+ // The operation achieved a quorum in favor.
+ if (vote === "for") {
+ return { attempts, start };
+ }
+
+ // Wait before reattempting.
+ if (attempts.length < maxAttempts) {
+ await new Promise((resolve) => {
+ setTimeout(
+ resolve,
+ Math.max(0, settings.retryDelay + Math.floor((Math.random() * 2 - 1) * settings.retryJitter)),
+ undefined
+ );
+ });
+ } else {
+ throw new ExecutionError("The operation was unable to achieve a quorum during its retry window.", attempts);
+ }
+ }
+ }
+
+ private async _attemptOperation(
+ script: { value: string; hash: string },
+ keys: string[],
+ args: (string | number)[]
+ ): Promise<
+ | { vote: "for"; stats: Promise; start: number }
+ | { vote: "against"; stats: Promise; start: number }
+ > {
+ const start = Date.now();
+
+ return await new Promise((resolve) => {
+ const clientResults = [];
+ for (const client of this.clients) {
+ clientResults.push(this._attemptOperationOnClient(client, script, keys, args));
+ }
+
+ const stats: ExecutionStats = {
+ membershipSize: clientResults.length,
+ quorumSize: Math.floor(clientResults.length / 2) + 1,
+ votesFor: new Set(),
+ votesAgainst: new Map()
+ };
+
+ let done: () => void;
+ const statsPromise = new Promise((resolve) => {
+ done = () => resolve(stats);
+ });
+
+ // This is the expected flow for all successful and unsuccessful requests.
+ const onResultResolve = (clientResult: ClientExecutionResult): void => {
+ switch (clientResult.vote) {
+ case "for":
+ stats.votesFor.add(clientResult.client);
+ break;
+ case "against":
+ stats.votesAgainst.set(clientResult.client, clientResult.error);
+ break;
+ }
+
+ // A quorum has determined a success.
+ if (stats.votesFor.size === stats.quorumSize) {
+ resolve({
+ vote: "for",
+ stats: statsPromise,
+ start
+ });
+ }
+
+ // A quorum has determined a failure.
+ if (stats.votesAgainst.size === stats.quorumSize) {
+ resolve({
+ vote: "against",
+ stats: statsPromise,
+ start
+ });
+ }
+
+ // All votes are in.
+ if (stats.votesFor.size + stats.votesAgainst.size === stats.membershipSize) {
+ done();
+ }
+ };
+
+ // This is unexpected and should crash to prevent undefined behavior.
+ const onResultReject = (error: Error): void => {
+ throw error;
+ };
+
+ for (const result of clientResults) {
+ result.then(onResultResolve, onResultReject);
+ }
+ });
+ }
+
+ private async _attemptOperationOnClient(
+ client: Client,
+ script: { value: string; hash: string },
+ keys: string[],
+ args: (string | number)[]
+ ): Promise {
+ try {
+ let result: number;
+ try {
+ // Attempt to evaluate the script by its hash.
+ // @ts-expect-error
+ const shaResult = (await client.evalsha(script.hash, keys.length, [...keys, ...args])) as unknown;
+
+ if (typeof shaResult !== "number") {
+ throw new Error(`Unexpected result of type ${typeof shaResult} returned from redis.`);
+ }
+
+ result = shaResult;
+ } catch (error) {
+ // If the redis server does not already have the script cached,
+ // reattempt the request with the script's raw text.
+ if (!(error instanceof Error) || !error.message.startsWith("NOSCRIPT")) {
+ throw error;
+ }
+ // @ts-expect-error
+ const rawResult = (await client.eval(script.value, keys.length, [...keys, ...args])) as unknown;
+
+ if (typeof rawResult !== "number") {
+ throw new Error(`Unexpected result of type ${typeof rawResult} returned from redis.`);
+ }
+
+ result = rawResult;
+ }
+
+ // One or more of the resources was already locked.
+ if (result !== keys.length) {
+ throw new ResourceLockedError(
+ `The operation was applied to: ${result} of the ${keys.length} requested resources.`
+ );
+ }
+
+ return {
+ vote: "for",
+ client,
+ value: result
+ };
+ } catch (error) {
+ if (!(error instanceof Error)) {
+ throw new Error(`Unexpected type ${typeof error} thrown with value: ${error}`);
+ }
+
+ // Emit the error on the redlock instance for observability.
+ this.emit("error", error);
+
+ return {
+ vote: "against",
+ client,
+ error
+ };
+ }
+ }
+
+ /**
+ * Wrap and execute a routine in the context of an auto-extending lock,
+ * returning a promise of the routine's value. In the case that auto-extension
+ * fails, an AbortSignal will be updated to indicate that abortion of the
+ * routine is in order, and to pass along the encountered error.
+ *
+ * @example
+ * ```ts
+ * await redlock.using([senderId, recipientId], 5000, { retryCount: 5 }, async (signal) => {
+ * const senderBalance = await getBalance(senderId);
+ * const recipientBalance = await getBalance(recipientId);
+ *
+ * if (senderBalance < amountToSend) {
+ * throw new Error("Insufficient balance.");
+ * }
+ *
+ * // The abort signal will be true if:
+ * // 1. the above took long enough that the lock needed to be extended
+ * // 2. redlock was unable to extend the lock
+ * //
+ * // In such a case, exclusivity can no longer be guaranteed for further
+ * // operations, and should be handled as an exceptional case.
+ * if (signal.aborted) {
+ * throw signal.error;
+ * }
+ *
+ * await setBalances([
+ * {id: senderId, balance: senderBalance - amountToSend},
+ * {id: recipientId, balance: recipientBalance + amountToSend},
+ * ]);
+ * });
+ * ```
+ */
+
+ public async using(
+ resources: string[],
+ duration: number,
+ settings: Partial,
+ routine?: (signal: RedlockAbortSignal) => Promise
+ ): Promise;
+
+ public async using(
+ resources: string[],
+ duration: number,
+ routine: (signal: RedlockAbortSignal) => Promise
+ ): Promise;
+
+ public async using(
+ resources: string[],
+ duration: number,
+ settingsOrRoutine: undefined | Partial | ((signal: RedlockAbortSignal) => Promise),
+ optionalRoutine?: (signal: RedlockAbortSignal) => Promise
+ ): Promise {
+ if (Math.floor(duration) !== duration) {
+ throw new Error("Duration must be an integer value in milliseconds.");
+ }
+
+ const settings =
+ settingsOrRoutine && typeof settingsOrRoutine !== "function"
+ ? {
+ ...this.settings,
+ ...settingsOrRoutine
+ }
+ : this.settings;
+
+ const routine = optionalRoutine ?? settingsOrRoutine;
+ if (typeof routine !== "function") {
+ throw new Error("INVARIANT: routine is not a function.");
+ }
+
+ if (settings.automaticExtensionThreshold > duration - 100) {
+ throw new Error(
+ "A lock `duration` must be at least 100ms greater than the `automaticExtensionThreshold` setting."
+ );
+ }
+
+ // The AbortController/AbortSignal pattern allows the routine to be notified
+ // of a failure to extend the lock, and subsequent expiration. In the event
+ // of an abort, the error object will be made available at `signal.error`.
+ const controller = new AbortController();
+
+ const signal = controller.signal as RedlockAbortSignal;
+
+ function queue(): void {
+ timeout = setTimeout(
+ () => (extension = extend()),
+ lock.expiration - Date.now() - settings.automaticExtensionThreshold
+ );
+ }
+
+ async function extend(): Promise {
+ timeout = undefined;
+
+ try {
+ lock = await lock.extend(duration);
+ queue();
+ } catch (error) {
+ if (!(error instanceof Error)) {
+ throw new Error(`Unexpected thrown ${typeof error}: ${error}.`);
+ }
+
+ if (lock.expiration > Date.now()) {
+ return (extension = extend());
+ }
+
+ signal.error = error instanceof Error ? error : new Error(`${error}`);
+ controller.abort();
+ }
+ }
+
+ let timeout: undefined | NodeJS.Timeout;
+ let extension: undefined | Promise;
+ let lock = await this.acquire(resources, duration, settings);
+ queue();
+
+ try {
+ return await routine(signal);
+ } finally {
+ // Clean up the timer.
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = undefined;
+ }
+
+ // Wait for an in-flight extension to finish.
+ if (extension) {
+ await extension.catch(() => {
+ // An error here doesn't matter at all, because the routine has
+ // already completed, and a release will be attempted regardless. The
+ // only reason for waiting here is to prevent possible contention
+ // between the extension and release.
+ });
+ }
+
+ await lock.release();
+ }
+ }
+}
diff --git a/backend/src/lib/zod/index.ts b/backend/src/lib/zod/index.ts
index a3cded66b..4d3fea8c7 100644
--- a/backend/src/lib/zod/index.ts
+++ b/backend/src/lib/zod/index.ts
@@ -7,3 +7,7 @@ export const zpStr = (schema: T, opt: { stripNull: boolean
if (typeof val !== "string") return val;
return val.trim() || undefined;
}, schema);
+
+export const zodBuffer = z.custom((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, {
+ message: "Expected binary data (Buffer Or Uint8Array)"
+});
diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts
index bc8ac88ff..d51a8e683 100644
--- a/backend/src/queue/queue-service.ts
+++ b/backend/src/queue/queue-service.ts
@@ -7,33 +7,44 @@ import {
TScanFullRepoEventPayload,
TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
+import { TSyncSecretsDTO } from "@app/services/secret/secret-types";
export enum QueueName {
SecretRotation = "secret-rotation",
SecretReminder = "secret-reminder",
AuditLog = "audit-log",
+ // TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
AuditLogPrune = "audit-log-prune",
+ DailyResourceCleanUp = "daily-resource-cleanup",
TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook",
SecretFullRepoScan = "secret-full-repo-scan",
SecretPushEventScan = "secret-push-event-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost",
- DynamicSecretRevocation = "dynamic-secret-revocation"
+ DynamicSecretRevocation = "dynamic-secret-revocation",
+ CaCrlRotation = "ca-crl-rotation",
+ SecretReplication = "secret-replication",
+ SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
}
export enum QueueJobs {
SecretReminder = "secret-reminder-job",
SecretRotation = "secret-rotation-job",
AuditLog = "audit-log-job",
+ // TODO(akhilmhdh): This will get removed later. For now this is kept to stop the repeatable queue
AuditLogPrune = "audit-log-prune-job",
+ DailyResourceCleanUp = "daily-resource-cleanup-job",
SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull",
SecretScan = "secret-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
DynamicSecretRevocation = "dynamic-secret-revocation",
- DynamicSecretPruning = "dynamic-secret-pruning"
+ DynamicSecretPruning = "dynamic-secret-pruning",
+ CaCrlRotation = "ca-crl-rotation-job",
+ SecretReplication = "secret-replication",
+ SecretSync = "secret-sync" // parent queue to push integration sync, webhook, and secret replication
}
export type TQueueJobTypes = {
@@ -46,7 +57,6 @@ export type TQueueJobTypes = {
};
name: QueueJobs.SecretReminder;
};
-
[QueueName.SecretRotation]: {
payload: { rotationId: string };
name: QueueJobs.SecretRotation;
@@ -55,6 +65,10 @@ export type TQueueJobTypes = {
name: QueueJobs.AuditLog;
payload: TCreateAuditLogDTO;
};
+ [QueueName.DailyResourceCleanUp]: {
+ name: QueueJobs.DailyResourceCleanUp;
+ payload: undefined;
+ };
[QueueName.AuditLogPrune]: {
name: QueueJobs.AuditLogPrune;
payload: undefined;
@@ -108,6 +122,20 @@ export type TQueueJobTypes = {
dynamicSecretCfgId: string;
};
};
+ [QueueName.CaCrlRotation]: {
+ name: QueueJobs.CaCrlRotation;
+ payload: {
+ caId: string;
+ };
+ };
+ [QueueName.SecretReplication]: {
+ name: QueueJobs.SecretReplication;
+ payload: TSyncSecretsDTO;
+ };
+ [QueueName.SecretSync]: {
+ name: QueueJobs.SecretSync;
+ payload: TSyncSecretsDTO;
+ };
};
export type TQueueServiceFactory = ReturnType;
@@ -124,7 +152,7 @@ export const queueServiceFactory = (redisUrl: string) => {
const start = (
name: T,
- jobFn: (job: Job) => Promise,
+ jobFn: (job: Job, token?: string) => Promise,
queueSettings: Omit = {}
) => {
if (queueContainer[name]) {
@@ -158,7 +186,7 @@ export const queueServiceFactory = (redisUrl: string) => {
name: T,
job: TQueueJobTypes[T]["name"],
data: TQueueJobTypes[T]["payload"],
- opts: JobsOptions & { jobId?: string }
+ opts?: JobsOptions & { jobId?: string }
) => {
const q = queueContainer[name];
@@ -172,7 +200,9 @@ export const queueServiceFactory = (redisUrl: string) => {
jobId?: string
) => {
const q = queueContainer[name];
- return q.removeRepeatable(job, repeatOpt, jobId);
+ if (q) {
+ return q.removeRepeatable(job, repeatOpt, jobId);
+ }
};
const stopRepeatableJobByJobId = async (name: T, jobId: string) => {
diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts
index 51cef185a..863162c2b 100644
--- a/backend/src/server/app.ts
+++ b/backend/src/server/app.ts
@@ -71,6 +71,7 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
if (appCfg.isProductionMode) {
await server.register(ratelimiter, globalRateLimiterCfg());
}
+
await server.register(helmet, { contentSecurityPolicy: false });
await server.register(maintenanceMode);
diff --git a/backend/src/server/boot-strap-check.ts b/backend/src/server/boot-strap-check.ts
index 381e575ef..ceaef59e7 100644
--- a/backend/src/server/boot-strap-check.ts
+++ b/backend/src/server/boot-strap-check.ts
@@ -5,7 +5,6 @@ import { createTransport } from "nodemailer";
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
-import { getTlsOption } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
type BootstrapOpt = {
@@ -44,7 +43,7 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
console.info("Testing smtp connection");
const smtpCfg = formatSmtpConfig();
- await createTransport({ ...smtpCfg, ...getTlsOption(smtpCfg.host, smtpCfg.secure) })
+ await createTransport(smtpCfg)
.verify()
.then(async () => {
console.info("SMTP successfully connected");
diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts
index 6c92de62c..ad54a151a 100644
--- a/backend/src/server/config/rateLimiter.ts
+++ b/backend/src/server/config/rateLimiter.ts
@@ -1,6 +1,7 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis";
+import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
@@ -21,14 +22,14 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
// GET endpoints
export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
- max: 600,
+ max: () => getRateLimiterConfig().readLimit,
keyGenerator: (req) => req.realIp
};
// POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
- max: 50,
+ max: () => getRateLimiterConfig().writeLimit,
keyGenerator: (req) => req.realIp
};
@@ -36,25 +37,48 @@ export const writeLimit: RateLimitOptions = {
export const secretsLimit: RateLimitOptions = {
// secrets, folders, secret imports
timeWindow: 60 * 1000,
- max: 60,
+ max: () => getRateLimiterConfig().secretsLimit,
keyGenerator: (req) => req.realIp
};
export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
- max: 60,
+ max: () => getRateLimiterConfig().authRateLimit,
keyGenerator: (req) => req.realIp
};
export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
- max: 30,
+ max: () => getRateLimiterConfig().inviteUserRateLimit,
keyGenerator: (req) => req.realIp
};
+export const mfaRateLimit: RateLimitOptions = {
+ timeWindow: 60 * 1000,
+ max: () => getRateLimiterConfig().mfaRateLimit,
+ keyGenerator: (req) => {
+ return req.headers.authorization?.split(" ")[1] || req.realIp;
+ }
+};
+
export const creationLimit: RateLimitOptions = {
// identity, project, org
timeWindow: 60 * 1000,
- max: 30,
+ max: () => getRateLimiterConfig().creationLimit,
+ keyGenerator: (req) => req.realIp
+};
+
+// Public endpoints to avoid brute force attacks
+export const publicEndpointLimit: RateLimitOptions = {
+ // Read Shared Secrets
+ timeWindow: 60 * 1000,
+ max: () => getRateLimiterConfig().publicEndpointLimit,
+ keyGenerator: (req) => req.realIp
+};
+
+export const publicSecretShareCreationLimit: RateLimitOptions = {
+ // Create Shared Secrets
+ timeWindow: 60 * 1000,
+ max: 5,
keyGenerator: (req) => req.realIp
};
diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts
index c8da4077a..3320c7d87 100644
--- a/backend/src/server/plugins/error-handler.ts
+++ b/backend/src/server/plugins/error-handler.ts
@@ -6,6 +6,7 @@ import {
BadRequestError,
DatabaseError,
InternalServerError,
+ NotFoundError,
ScimRequestError,
UnauthorizedError
} from "@app/lib/errors";
@@ -15,6 +16,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
req.log.error(error);
if (error instanceof BadRequestError) {
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name });
+ } else if (error instanceof NotFoundError) {
+ void res.status(404).send({ statusCode: 404, message: error.message, error: error.name });
} else if (error instanceof UnauthorizedError) {
void res.status(403).send({ statusCode: 403, message: error.message, error: error.name });
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
diff --git a/backend/src/server/plugins/ip.ts b/backend/src/server/plugins/ip.ts
index b3c8171af..7b5838d57 100644
--- a/backend/src/server/plugins/ip.ts
+++ b/backend/src/server/plugins/ip.ts
@@ -6,6 +6,7 @@ const headersOrder = [
"cf-connecting-ip", // Cloudflare
"Cf-Pseudo-IPv4", // Cloudflare
"x-client-ip", // Most common
+ "x-envoy-external-address", // for envoy
"x-forwarded-for", // Mostly used by proxies
"fastly-client-ip",
"true-client-ip", // Akamai and Cloudflare
@@ -23,7 +24,21 @@ export const fastifyIp = fp(async (fastify) => {
const forwardedIpHeader = headersOrder.find((header) => Boolean(req.headers[header]));
const forwardedIp = forwardedIpHeader ? req.headers[forwardedIpHeader] : undefined;
if (forwardedIp) {
- req.realIp = Array.isArray(forwardedIp) ? forwardedIp[0] : forwardedIp;
+ if (Array.isArray(forwardedIp)) {
+ // eslint-disable-next-line
+ req.realIp = forwardedIp[0];
+ return;
+ }
+
+ if (forwardedIp.includes(",")) {
+ // the ip header when placed with load balancers that proxy request
+ // will attach the internal ips to header by appending with comma
+ // https://github.com/go-chi/chi/blob/master/middleware/realip.go
+ const clientIPFromProxy = forwardedIp.slice(0, forwardedIp.indexOf(",")).trim();
+ req.realIp = clientIPFromProxy;
+ return;
+ }
+ req.realIp = forwardedIp;
} else {
req.realIp = req.ip;
}
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 75c43a9aa..326fbafa6 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -1,3 +1,4 @@
+import { CronJob } from "cron";
import { Knex } from "knex";
import { z } from "zod";
@@ -13,6 +14,8 @@ import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-lo
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
+import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
+import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
@@ -33,6 +36,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
+import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal";
+import { rateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
@@ -44,6 +49,7 @@ import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approva
import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal";
import { secretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
import { secretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
+import { secretReplicationServiceFactory } from "@app/ee/services/secret-replication/secret-replication-service";
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
@@ -70,6 +76,14 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
+import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
+import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
+import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
+import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
+import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
+import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
+import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
+import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
@@ -80,6 +94,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
+import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
+import { identityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
@@ -94,6 +110,9 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal
import { integrationServiceFactory } from "@app/services/integration/integration-service";
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
+import { kmsDALFactory } from "@app/services/kms/kms-dal";
+import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
+import { kmsServiceFactory } from "@app/services/kms/kms-service";
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";
@@ -115,6 +134,7 @@ import { projectMembershipServiceFactory } from "@app/services/project-membershi
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
+import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
import { secretDALFactory } from "@app/services/secret/secret-dal";
import { secretQueueFactory } from "@app/services/secret/secret-queue";
import { secretServiceFactory } from "@app/services/secret/secret-service";
@@ -127,6 +147,8 @@ import { secretFolderServiceFactory } from "@app/services/secret-folder/secret-f
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
import { secretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
+import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
+import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
@@ -176,6 +198,7 @@ export const registerRoutes = async (
const incidentContactDAL = incidentContactDALFactory(db);
const orgRoleDAL = orgRoleDALFactory(db);
const superAdminDAL = superAdminDALFactory(db);
+ const rateLimitDAL = rateLimitDALFactory(db);
const apiKeyDAL = apiKeyDALFactory(db);
const projectDAL = projectDALFactory(db);
@@ -212,8 +235,8 @@ export const registerRoutes = async (
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
-
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
+ const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(db);
const auditLogStreamDAL = auditLogStreamDALFactory(db);
@@ -235,8 +258,8 @@ export const registerRoutes = async (
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
- const sarReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
- const sarSecretDAL = secretApprovalRequestSecretDALFactory(db);
+ const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
+ const secretApprovalRequestSecretDAL = secretApprovalRequestSecretDALFactory(db);
const secretRotationDAL = secretRotationDALFactory(db);
const snapshotDAL = snapshotDALFactory(db);
@@ -250,10 +273,14 @@ export const registerRoutes = async (
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
const secretScanningDAL = secretScanningDALFactory(db);
+ const secretSharingDAL = secretSharingDALFactory(db);
const licenseDAL = licenseDALFactory(db);
const dynamicSecretDAL = dynamicSecretDALFactory(db);
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
+ const kmsDAL = kmsDALFactory(db);
+ const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
+
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@@ -262,6 +289,12 @@ export const registerRoutes = async (
projectDAL
});
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
+ const kmsService = kmsServiceFactory({
+ kmsRootConfigDAL,
+ keyStore,
+ kmsDAL
+ });
+
const trustedIpService = trustedIpServiceFactory({
licenseService,
projectDAL,
@@ -282,7 +315,7 @@ export const registerRoutes = async (
permissionService,
auditLogStreamDAL
});
- const sapService = secretApprovalPolicyServiceFactory({
+ const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({
projectMembershipDAL,
projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
@@ -425,6 +458,10 @@ export const registerRoutes = async (
orgService,
keyStore
});
+ const rateLimitService = rateLimitServiceFactory({
+ rateLimitDAL,
+ licenseService
+ });
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
const secretScanningQueue = secretScanningQueueFactory({
@@ -483,10 +520,62 @@ export const registerRoutes = async (
projectBotDAL,
projectMembershipDAL,
secretApprovalRequestDAL,
- secretApprovalSecretDAL: sarSecretDAL,
+ secretApprovalSecretDAL: secretApprovalRequestSecretDAL,
projectUserMembershipRoleDAL
});
+ const certificateAuthorityDAL = certificateAuthorityDALFactory(db);
+ const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
+ const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
+ const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
+
+ const certificateDAL = certificateDALFactory(db);
+ const certificateBodyDAL = certificateBodyDALFactory(db);
+
+ const certificateService = certificateServiceFactory({
+ certificateDAL,
+ certificateBodyDAL,
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ certificateAuthorityCrlDAL,
+ certificateAuthoritySecretDAL,
+ projectDAL,
+ kmsService,
+ permissionService
+ });
+
+ const certificateAuthorityQueue = certificateAuthorityQueueFactory({
+ certificateAuthorityCrlDAL,
+ certificateAuthorityDAL,
+ certificateAuthoritySecretDAL,
+ certificateDAL,
+ projectDAL,
+ kmsService,
+ queueService
+ });
+
+ const certificateAuthorityService = certificateAuthorityServiceFactory({
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ certificateAuthoritySecretDAL,
+ certificateAuthorityCrlDAL,
+ certificateAuthorityQueue,
+ certificateDAL,
+ certificateBodyDAL,
+ projectDAL,
+ kmsService,
+ permissionService
+ });
+
+ const certificateAuthorityCrlService = certificateAuthorityCrlServiceFactory({
+ certificateAuthorityDAL,
+ certificateAuthorityCrlDAL,
+ projectDAL,
+ kmsService,
+ permissionService,
+ licenseService
+ });
+
const projectService = projectServiceFactory({
permissionService,
projectDAL,
@@ -503,6 +592,8 @@ export const registerRoutes = async (
projectMembershipDAL,
folderDAL,
licenseService,
+ certificateAuthorityDAL,
+ certificateDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
keyStore
@@ -520,7 +611,8 @@ export const registerRoutes = async (
permissionService,
projectRoleDAL,
projectUserMembershipRoleDAL,
- identityProjectMembershipRoleDAL
+ identityProjectMembershipRoleDAL,
+ projectDAL
});
const snapshotService = secretSnapshotServiceFactory({
@@ -580,6 +672,7 @@ export const registerRoutes = async (
secretVersionTagDAL
});
const secretImportService = secretImportServiceFactory({
+ licenseService,
projectEnvDAL,
folderDAL,
permissionService,
@@ -608,19 +701,24 @@ export const registerRoutes = async (
projectEnvDAL,
projectBotService
});
- const sarService = secretApprovalRequestServiceFactory({
+
+ const secretSharingService = secretSharingServiceFactory({
+ permissionService,
+ secretSharingDAL
+ });
+
+ const secretApprovalRequestService = secretApprovalRequestServiceFactory({
permissionService,
projectBotService,
folderDAL,
secretDAL,
secretTagDAL,
- secretApprovalRequestSecretDAL: sarSecretDAL,
- secretApprovalRequestReviewerDAL: sarReviewerDAL,
+ secretApprovalRequestSecretDAL,
+ secretApprovalRequestReviewerDAL,
projectDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretApprovalRequestDAL,
- secretService,
snapshotService,
secretVersionTagDAL,
secretQueueService
@@ -649,6 +747,23 @@ export const registerRoutes = async (
accessApprovalPolicyApproverDAL
});
+ const secretReplicationService = secretReplicationServiceFactory({
+ secretTagDAL,
+ secretVersionTagDAL,
+ secretDAL,
+ secretVersionDAL,
+ secretImportDAL,
+ keyStore,
+ queueService,
+ folderDAL,
+ secretApprovalPolicyService,
+ secretBlindIndexDAL,
+ secretApprovalRequestDAL,
+ secretApprovalRequestSecretDAL,
+ secretQueueService,
+ projectMembershipDAL,
+ projectBotService
+ });
const secretRotationQueue = secretRotationQueueFactory({
telemetryService,
secretRotationDAL,
@@ -742,6 +857,15 @@ export const registerRoutes = async (
permissionService
});
+ const identityAzureAuthService = identityAzureAuthServiceFactory({
+ identityAzureAuthDAL,
+ identityOrgMembershipDAL,
+ identityAccessTokenDAL,
+ identityDAL,
+ permissionService,
+ licenseService
+ });
+
const dynamicSecretProviders = buildDynamicSecretProviders();
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
queueService,
@@ -769,14 +893,24 @@ export const registerRoutes = async (
folderDAL,
licenseService
});
+ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
+ auditLogDAL,
+ queueService,
+ secretVersionDAL,
+ secretFolderVersionDAL: folderVersionDAL,
+ snapshotDAL,
+ identityAccessTokenDAL,
+ secretSharingDAL
+ });
await superAdminService.initServerCfg();
//
// setup the communication with license key server
await licenseService.init();
- await auditLogQueue.startAuditLogPruneJob();
await telemetryQueue.startTelemetryCheck();
+ await dailyResourceCleanUp.startCleanUp();
+ await kmsService.startService();
// inject all services
server.decorate("services", {
@@ -798,7 +932,9 @@ export const registerRoutes = async (
projectEnv: projectEnvService,
projectRole: projectRoleService,
secret: secretService,
+ secretReplication: secretReplicationService,
secretTag: secretTagService,
+ rateLimit: rateLimitService,
folder: folderService,
secretImport: secretImportService,
projectBot: projectBotService,
@@ -813,10 +949,11 @@ export const registerRoutes = async (
identityKubernetesAuth: identityKubernetesAuthService,
identityGcpAuth: identityGcpAuthService,
identityAwsAuth: identityAwsAuthService,
- secretApprovalPolicy: sapService,
+ identityAzureAuth: identityAzureAuthService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
- secretApprovalRequest: sarService,
+ secretApprovalPolicy: secretApprovalPolicyService,
+ secretApprovalRequest: secretApprovalRequestService,
secretRotation: secretRotationService,
dynamicSecret: dynamicSecretService,
dynamicSecretLease: dynamicSecretLeaseService,
@@ -825,6 +962,9 @@ export const registerRoutes = async (
ldap: ldapService,
auditLog: auditLogService,
auditLogStream: auditLogStreamService,
+ certificate: certificateService,
+ certificateAuthority: certificateAuthorityService,
+ certificateAuthorityCrl: certificateAuthorityCrlService,
secretScanning: secretScanningService,
license: licenseService,
trustedIp: trustedIpService,
@@ -832,9 +972,18 @@ export const registerRoutes = async (
secretBlindIndex: secretBlindIndexService,
telemetry: telemetryService,
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
- identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService
+ identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
+ secretSharing: secretSharingService
});
+ const cronJobs: CronJob[] = [];
+ if (appCfg.isProductionMode) {
+ const rateLimitSyncJob = await rateLimitService.initializeBackgroundSync();
+ if (rateLimitSyncJob) {
+ cronJobs.push(rateLimitSyncJob);
+ }
+ }
+
server.decorate("store", {
user: userDAL
});
@@ -857,7 +1006,8 @@ export const registerRoutes = async (
emailConfigured: z.boolean().optional(),
inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(),
- secretScanningConfigured: z.boolean().optional()
+ secretScanningConfigured: z.boolean().optional(),
+ samlDefaultOrgSlug: z.string().optional()
})
}
},
@@ -870,7 +1020,8 @@ export const registerRoutes = async (
emailConfigured: cfg.isSmtpConfigured,
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured,
- secretScanningConfigured: cfg.isSecretScanningConfigured
+ secretScanningConfigured: cfg.isSecretScanningConfigured,
+ samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
};
}
});
@@ -887,6 +1038,7 @@ export const registerRoutes = async (
await server.register(registerV3Routes, { prefix: "/api/v3" });
server.addHook("onClose", async () => {
+ cronJobs.forEach((job) => job.stop());
await telemetryService.flushAll();
});
};
diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts
index cf9f23851..5b0b754f3 100644
--- a/backend/src/server/routes/sanitizedSchemas.ts
+++ b/backend/src/server/routes/sanitizedSchemas.ts
@@ -4,6 +4,7 @@ import {
DynamicSecretsSchema,
IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema,
+ ProjectRolesSchema,
SecretApprovalPoliciesSchema,
UsersSchema
} from "@app/db/schemas";
@@ -88,10 +89,38 @@ export const ProjectPermissionSchema = z.object({
.optional()
});
+export const ProjectSpecificPrivilegePermissionSchema = z.object({
+ actions: z
+ .nativeEnum(ProjectPermissionActions)
+ .describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read")
+ .array()
+ .min(1),
+ subject: z
+ .enum([ProjectPermissionSub.Secrets])
+ .describe("The entity this permission pertains to. Possible options: secrets, environments"),
+ conditions: z
+ .object({
+ environment: z.string().describe("The environment slug this permission should allow."),
+ secretPath: z
+ .object({
+ $glob: z
+ .string()
+ .min(1)
+ .describe("The secret path this permission should allow. Can be a glob pattern such as /folder-name/*/** ")
+ })
+ .optional()
+ })
+ .describe("When specified, only matching conditions will be allowed to access given resource.")
+});
+
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
permissions: UnpackedPermissionSchema.array()
});
+export const SanitizedRoleSchema = ProjectRolesSchema.extend({
+ permissions: UnpackedPermissionSchema.array()
+});
+
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index 572409d9b..97c3449d9 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -79,6 +79,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
email: z.string().email().trim(),
+ password: z.string().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts
new file mode 100644
index 000000000..7573c0bd2
--- /dev/null
+++ b/backend/src/server/routes/v1/certificate-authority-router.ts
@@ -0,0 +1,515 @@
+import ms from "ms";
+import { z } from "zod";
+
+import { CertificateAuthoritiesSchema } from "@app/db/schemas";
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
+import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
+import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
+
+export const registerCaRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Create CA",
+ body: z
+ .object({
+ projectSlug: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.projectSlug),
+ type: z.nativeEnum(CaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type),
+ friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
+ commonName: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.commonName),
+ organization: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.organization),
+ ou: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.ou),
+ country: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.country),
+ province: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.province),
+ locality: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.locality),
+ // format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
+ notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.CREATE.notBefore),
+ notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.CREATE.notAfter),
+ maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.CREATE.maxPathLength),
+ keyAlgorithm: z
+ .nativeEnum(CertKeyAlgorithm)
+ .default(CertKeyAlgorithm.RSA_2048)
+ .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
+ })
+ .refine(
+ (data) => {
+ // Check that at least one of the specified fields is non-empty
+ return [data.commonName, data.organization, data.ou, data.country, data.province, data.locality].some(
+ (field) => field !== ""
+ );
+ },
+ {
+ message:
+ "At least one of the fields commonName, organization, ou, country, province, or locality must be non-empty",
+ path: []
+ }
+ ),
+ response: {
+ 200: z.object({
+ ca: CertificateAuthoritiesSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const ca = await server.services.certificateAuthority.createCa({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.CREATE_CA,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn
+ }
+ }
+ });
+
+ return {
+ ca
+ };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:caId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET.caId)
+ }),
+ response: {
+ 200: z.object({
+ ca: CertificateAuthoritiesSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const ca = await server.services.certificateAuthority.getCaById({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.GET_CA,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn
+ }
+ }
+ });
+
+ return {
+ ca
+ };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/:caId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Update CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId)
+ }),
+ body: z.object({
+ status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status)
+ }),
+ response: {
+ 200: z.object({
+ ca: CertificateAuthoritiesSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const ca = await server.services.certificateAuthority.updateCaById({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.UPDATE_CA,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn,
+ status: ca.status as CaStatus
+ }
+ }
+ });
+
+ return {
+ ca
+ };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:caId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Delete CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.DELETE.caId)
+ }),
+ response: {
+ 200: z.object({
+ ca: CertificateAuthoritiesSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const ca = await server.services.certificateAuthority.deleteCaById({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.DELETE_CA,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn
+ }
+ }
+ });
+
+ return {
+ ca
+ };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:caId/csr",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get CA CSR",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CSR.caId)
+ }),
+ response: {
+ 200: z.object({
+ csr: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CSR.csr)
+ })
+ }
+ },
+ handler: async (req) => {
+ const { ca, csr } = await server.services.certificateAuthority.getCaCsr({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.GET_CA_CSR,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn
+ }
+ }
+ });
+
+ return {
+ csr
+ };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:caId/certificate",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get cert and cert chain of a CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
+ }),
+ response: {
+ 200: z.object({
+ certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate),
+ certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain),
+ serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber)
+ })
+ }
+ },
+ handler: async (req) => {
+ const { certificate, certificateChain, serialNumber, ca } = await server.services.certificateAuthority.getCaCert({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.GET_CA_CERT,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn
+ }
+ }
+ });
+
+ return {
+ certificate,
+ certificateChain,
+ serialNumber
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/:caId/sign-intermediate",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Create intermediate CA certificate from parent CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
+ }),
+ body: z.object({
+ csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
+ notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
+ notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
+ maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
+ }),
+ response: {
+ 200: z.object({
+ certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificate),
+ certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificateChain),
+ issuingCaCertificate: z
+ .string()
+ .trim()
+ .describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.issuingCaCertificate),
+ serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.serialNumber)
+ })
+ }
+ },
+ handler: async (req) => {
+ const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
+ await server.services.certificateAuthority.signIntermediate({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.SIGN_INTERMEDIATE,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn,
+ serialNumber
+ }
+ }
+ });
+
+ return {
+ certificate,
+ certificateChain,
+ issuingCaCertificate,
+ serialNumber
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/:caId/import-certificate",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Import certificate and chain to CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.caId)
+ }),
+ body: z.object({
+ certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificate),
+ certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificateChain)
+ }),
+ response: {
+ 200: z.object({
+ message: z.string().trim(),
+ caId: z.string().trim()
+ })
+ }
+ },
+ handler: async (req) => {
+ const { ca } = await server.services.certificateAuthority.importCertToCa({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.IMPORT_CA_CERT,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn
+ }
+ }
+ });
+
+ return {
+ message: "Successfully imported certificate to CA",
+ caId: req.params.caId
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/:caId/issue-certificate",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Issue certificate from CA",
+ params: z.object({
+ caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId)
+ }),
+ body: z
+ .object({
+ friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
+ commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
+ ttl: z
+ .string()
+ .refine((val) => ms(val) > 0, "TTL must be a positive number")
+ .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
+ notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
+ notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
+ })
+ .refine(
+ (data) => {
+ const { ttl, notAfter } = data;
+ return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
+ },
+ {
+ message: "Either ttl or notAfter must be present, but not both",
+ path: ["ttl", "notAfter"]
+ }
+ ),
+ response: {
+ 200: z.object({
+ certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
+ issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
+ certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
+ privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
+ serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
+ })
+ }
+ },
+ handler: async (req) => {
+ const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
+ await server.services.certificateAuthority.issueCertFromCa({
+ caId: req.params.caId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.ISSUE_CERT,
+ metadata: {
+ caId: ca.id,
+ dn: ca.dn,
+ serialNumber
+ }
+ }
+ });
+
+ return {
+ certificate,
+ certificateChain,
+ issuingCaCertificate,
+ privateKey,
+ serialNumber
+ };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts
new file mode 100644
index 000000000..938fbf7fe
--- /dev/null
+++ b/backend/src/server/routes/v1/certificate-router.ts
@@ -0,0 +1,207 @@
+import { z } from "zod";
+
+import { CertificatesSchema } from "@app/db/schemas";
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { CERTIFICATES } from "@app/lib/api-docs";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { CrlReason } from "@app/services/certificate/certificate-types";
+
+export const registerCertRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "GET",
+ url: "/:serialNumber",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get certificate",
+ params: z.object({
+ serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
+ }),
+ response: {
+ 200: z.object({
+ certificate: CertificatesSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const { cert, ca } = await server.services.certificate.getCert({
+ serialNumber: req.params.serialNumber,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.GET_CERT,
+ metadata: {
+ certId: cert.id,
+ cn: cert.commonName,
+ serialNumber: cert.serialNumber
+ }
+ }
+ });
+
+ return {
+ certificate: cert
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/:serialNumber/revoke",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Revoke",
+ params: z.object({
+ serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber)
+ }),
+ body: z.object({
+ revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason)
+ }),
+ response: {
+ 200: z.object({
+ message: z.string().trim(),
+ serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumberRes),
+ revokedAt: z.date().describe(CERTIFICATES.REVOKE.revokedAt)
+ })
+ }
+ },
+ handler: async (req) => {
+ const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({
+ serialNumber: req.params.serialNumber,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.REVOKE_CERT,
+ metadata: {
+ certId: cert.id,
+ cn: cert.commonName,
+ serialNumber: cert.serialNumber
+ }
+ }
+ });
+
+ return {
+ message: "Successfully revoked certificate",
+ serialNumber: req.params.serialNumber,
+ revokedAt
+ };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:serialNumber",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Delete certificate",
+ params: z.object({
+ serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber)
+ }),
+ response: {
+ 200: z.object({
+ certificate: CertificatesSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const { deletedCert, ca } = await server.services.certificate.deleteCert({
+ serialNumber: req.params.serialNumber,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.DELETE_CERT,
+ metadata: {
+ certId: deletedCert.id,
+ cn: deletedCert.commonName,
+ serialNumber: deletedCert.serialNumber
+ }
+ }
+ });
+
+ return {
+ certificate: deletedCert
+ };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:serialNumber/certificate",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Get certificate body of certificate",
+ params: z.object({
+ serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
+ }),
+ response: {
+ 200: z.object({
+ certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
+ certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain),
+ serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
+ })
+ }
+ },
+ handler: async (req) => {
+ const { certificate, certificateChain, serialNumber, cert, ca } = await server.services.certificate.getCertBody({
+ serialNumber: req.params.serialNumber,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ projectId: ca.projectId,
+ event: {
+ type: EventType.DELETE_CERT,
+ metadata: {
+ certId: cert.id,
+ cn: cert.commonName,
+ serialNumber: cert.serialNumber
+ }
+ }
+ });
+
+ return {
+ certificate,
+ certificateChain,
+ serialNumber
+ };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts
new file mode 100644
index 000000000..d10cd131b
--- /dev/null
+++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts
@@ -0,0 +1,262 @@
+import { z } from "zod";
+
+import { IdentityAzureAuthsSchema } from "@app/db/schemas";
+import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
+import { validateAzureAuthField } from "@app/services/identity-azure-auth/identity-azure-auth-validators";
+
+export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/azure-auth/login",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Login with Azure Auth",
+ body: z.object({
+ identityId: z.string(),
+ jwt: z.string()
+ }),
+ response: {
+ 200: z.object({
+ accessToken: z.string(),
+ expiresIn: z.coerce.number(),
+ accessTokenMaxTTL: z.coerce.number(),
+ tokenType: z.literal("Bearer")
+ })
+ }
+ },
+ handler: async (req) => {
+ const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } =
+ await server.services.identityAzureAuth.login(req.body);
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identityMembershipOrg.orgId,
+ event: {
+ type: EventType.LOGIN_IDENTITY_AZURE_AUTH,
+ metadata: {
+ identityId: identityAzureAuth.identityId,
+ identityAccessTokenId: identityAccessToken.id,
+ identityAzureAuthId: identityAzureAuth.id
+ }
+ }
+ });
+
+ return {
+ accessToken,
+ tokenType: "Bearer" as const,
+ expiresIn: identityAzureAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/azure-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Attach Azure Auth configuration onto identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim()
+ }),
+ body: z.object({
+ tenantId: z.string().trim(),
+ resource: z.string().trim(),
+ allowedServicePrincipalIds: validateAzureAuthField,
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
+ accessTokenTTL: z
+ .number()
+ .int()
+ .min(1)
+ .refine((value) => value !== 0, {
+ message: "accessTokenTTL must have a non zero number"
+ })
+ .default(2592000),
+ accessTokenMaxTTL: z
+ .number()
+ .int()
+ .refine((value) => value !== 0, {
+ message: "accessTokenMaxTTL must have a non zero number"
+ })
+ .default(2592000),
+ accessTokenNumUsesLimit: z.number().int().min(0).default(0)
+ }),
+ response: {
+ 200: z.object({
+ identityAzureAuth: IdentityAzureAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityAzureAuth = await server.services.identityAzureAuth.attachAzureAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body,
+ identityId: req.params.identityId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identityAzureAuth.orgId,
+ event: {
+ type: EventType.ADD_IDENTITY_AZURE_AUTH,
+ metadata: {
+ identityId: identityAzureAuth.identityId,
+ tenantId: identityAzureAuth.tenantId,
+ resource: identityAzureAuth.resource,
+ accessTokenTTL: identityAzureAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
+ accessTokenTrustedIps: identityAzureAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
+ accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
+ }
+ }
+ });
+
+ return { identityAzureAuth };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/azure-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Update Azure Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string().trim()
+ }),
+ body: z.object({
+ tenantId: z.string().trim().optional(),
+ resource: z.string().trim().optional(),
+ allowedServicePrincipalIds: validateAzureAuthField.optional(),
+ accessTokenTrustedIps: z
+ .object({
+ ipAddress: z.string().trim()
+ })
+ .array()
+ .min(1)
+ .optional(),
+ accessTokenTTL: z.number().int().min(0).optional(),
+ accessTokenNumUsesLimit: z.number().int().min(0).optional(),
+ accessTokenMaxTTL: z
+ .number()
+ .int()
+ .refine((value) => value !== 0, {
+ message: "accessTokenMaxTTL must have a non zero number"
+ })
+ .optional()
+ }),
+ response: {
+ 200: z.object({
+ identityAzureAuth: IdentityAzureAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityAzureAuth = await server.services.identityAzureAuth.updateAzureAuth({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body,
+ identityId: req.params.identityId
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identityAzureAuth.orgId,
+ event: {
+ type: EventType.UPDATE_IDENTITY_AZURE_AUTH,
+ metadata: {
+ identityId: identityAzureAuth.identityId,
+ tenantId: identityAzureAuth.tenantId,
+ resource: identityAzureAuth.resource,
+ accessTokenTTL: identityAzureAuth.accessTokenTTL,
+ accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
+ accessTokenTrustedIps: identityAzureAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
+ accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
+ }
+ }
+ });
+
+ return { identityAzureAuth };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/azure-auth/identities/:identityId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Retrieve Azure Auth configuration on identity",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ identityId: z.string()
+ }),
+ response: {
+ 200: z.object({
+ identityAzureAuth: IdentityAzureAuthsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const identityAzureAuth = await server.services.identityAzureAuth.getAzureAuth({
+ identityId: req.params.identityId,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod
+ });
+
+ await server.services.auditLog.createAuditLog({
+ ...req.auditLogInfo,
+ orgId: identityAzureAuth.orgId,
+ event: {
+ type: EventType.GET_IDENTITY_AZURE_AUTH,
+ metadata: {
+ identityId: identityAzureAuth.identityId
+ }
+ }
+ });
+
+ return { identityAzureAuth };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts
index 58654f220..34940eb13 100644
--- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts
+++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts
@@ -160,9 +160,9 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
}),
body: z.object({
type: z.enum(["iam", "gce"]).optional(),
- allowedServiceAccounts: validateGcpAuthField,
- allowedProjects: validateGcpAuthField,
- allowedZones: validateGcpAuthField,
+ allowedServiceAccounts: validateGcpAuthField.optional(),
+ allowedProjects: validateGcpAuthField.optional(),
+ allowedZones: validateGcpAuthField.optional(),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts
index d20ea0edc..227345916 100644
--- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts
+++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts
@@ -198,7 +198,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}),
response: {
200: z.object({
- identityKubernetesAuth: IdentityKubernetesAuthsSchema
+ identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
})
}
},
diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts
index 262e3cb20..eee7dac65 100644
--- a/backend/src/server/routes/v1/index.ts
+++ b/backend/src/server/routes/v1/index.ts
@@ -1,8 +1,11 @@
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router";
+import { registerCaRouter } from "./certificate-authority-router";
+import { registerCertRouter } from "./certificate-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
+import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityRouter } from "./identity-router";
@@ -18,6 +21,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerSecretFolderRouter } from "./secret-folder-router";
import { registerSecretImportRouter } from "./secret-import-router";
+import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSsoRouter } from "./sso-router";
import { registerUserActionRouter } from "./user-action-router";
@@ -34,6 +38,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityGcpAuthRouter);
await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter);
+ await authRouter.register(registerIdentityAzureAuthRouter);
},
{ prefix: "/auth" }
);
@@ -58,9 +63,18 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
{ prefix: "/workspace" }
);
+ await server.register(
+ async (pkiRouter) => {
+ await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
+ await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
+ },
+ { prefix: "/pki" }
+ );
+
await server.register(registerProjectBotRouter, { prefix: "/bot" });
await server.register(registerIntegrationRouter, { prefix: "/integration" });
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
await server.register(registerIdentityRouter, { prefix: "/identities" });
+ await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
};
diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts
index d9db7404e..899c1cac8 100644
--- a/backend/src/server/routes/v1/integration-auth-router.ts
+++ b/backend/src/server/routes/v1/integration-auth-router.ts
@@ -330,7 +330,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
teams: z
.object({
name: z.string(),
- id: z.string().optional()
+ id: z.string()
})
.array()
})
diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts
index f23abc45b..97a7f4d7a 100644
--- a/backend/src/server/routes/v1/integration-router.ts
+++ b/backend/src/server/routes/v1/integration-router.ts
@@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
-import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list";
+import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
@@ -42,39 +42,11 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService),
targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId),
owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner),
+ url: z.string().trim().optional().describe(INTEGRATION.CREATE.url),
path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
- metadata: z
- .object({
- secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
- secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
- initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
- mappingBehavior: z
- .nativeEnum(IntegrationMappingBehavior)
- .optional()
- .describe(INTEGRATION.CREATE.metadata.mappingBehavior),
- shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
- secretGCPLabel: z
- .object({
- labelName: z.string(),
- labelValue: z.string()
- })
- .optional()
- .describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
- secretAWSTag: z
- .array(
- z.object({
- key: z.string(),
- value: z.string()
- })
- )
- .optional()
- .describe(INTEGRATION.CREATE.metadata.secretAWSTag),
- kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
- shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
- })
- .default({})
+ metadata: IntegrationMetadataSchema.default({})
}),
response: {
200: z.object({
@@ -160,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
- metadata: z
- .object({
- secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
- secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
- initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
- mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior),
- shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
- secretGCPLabel: z
- .object({
- labelName: z.string(),
- labelValue: z.string()
- })
- .optional()
- .describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
- secretAWSTag: z
- .array(
- z.object({
- key: z.string(),
- value: z.string()
- })
- )
- .optional()
- .describe(INTEGRATION.CREATE.metadata.secretAWSTag),
- kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
- shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
- })
- .optional()
+ metadata: IntegrationMetadataSchema.optional()
}),
response: {
200: z.object({
diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts
index a8ef3fb77..c94e5d4cb 100644
--- a/backend/src/server/routes/v1/password-router.ts
+++ b/backend/src/server/routes/v1/password-router.ts
@@ -51,7 +51,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
- verifier: z.string().trim()
+ verifier: z.string().trim(),
+ password: z.string().trim()
}),
response: {
200: z.object({
diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts
index 6bbb8d7ef..4f92783c5 100644
--- a/backend/src/server/routes/v1/project-membership-router.ts
+++ b/backend/src/server/routes/v1/project-membership-router.ts
@@ -309,4 +309,32 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
return { membership };
}
});
+
+ server.route({
+ method: "DELETE",
+ url: "/:workspaceId/leave",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ workspaceId: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ membership: ProjectMembershipsSchema
+ })
+ }
+ },
+
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const membership = await server.services.projectMembership.leaveProject({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ projectId: req.params.workspaceId
+ });
+ return { membership };
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts
index 1cf655a97..0984b66f6 100644
--- a/backend/src/server/routes/v1/project-router.ts
+++ b/backend/src/server/routes/v1/project-router.ts
@@ -334,6 +334,44 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ method: "PUT",
+ url: "/:workspaceSlug/version-limit",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ workspaceSlug: z.string().trim()
+ }),
+ body: z.object({
+ pitVersionLimit: z.number().min(1).max(100)
+ }),
+ response: {
+ 200: z.object({
+ message: z.string(),
+ workspace: ProjectsSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const workspace = await server.services.project.updateVersionLimit({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ pitVersionLimit: req.body.pitVersionLimit,
+ workspaceSlug: req.params.workspaceSlug
+ });
+
+ return {
+ message: "Successfully changed workspace version limit",
+ workspace
+ };
+ }
+ });
+
server.route({
method: "GET",
url: "/:workspaceId/integrations",
diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v1/secret-import-router.ts
index d036fdbdd..ca604e738 100644
--- a/backend/src/server/routes/v1/secret-import-router.ts
+++ b/backend/src/server/routes/v1/secret-import-router.ts
@@ -29,7 +29,8 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
import: z.object({
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
- })
+ }),
+ isReplication: z.boolean().default(false).describe(SECRET_IMPORTS.CREATE.isReplication)
}),
response: {
200: z.object({
@@ -210,6 +211,49 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
}
});
+ server.route({
+ method: "POST",
+ url: "/:secretImportId/replication-resync",
+ config: {
+ rateLimit: secretsLimit
+ },
+ schema: {
+ description: "Resync secret replication of secret imports",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId)
+ }),
+ body: z.object({
+ workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.workspaceId),
+ environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path)
+ }),
+ response: {
+ 200: z.object({
+ message: z.string()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const { message } = await server.services.secretImport.resyncSecretImportReplication({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ id: req.params.secretImportId,
+ ...req.body,
+ projectId: req.body.workspaceId
+ });
+
+ return { message };
+ }
+ });
+
server.route({
method: "GET",
url: "/",
@@ -232,11 +276,9 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
200: z.object({
message: z.string(),
secretImports: SecretImportsSchema.omit({ importEnv: true })
- .merge(
- z.object({
- importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
- })
- )
+ .extend({
+ importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() })
+ })
.array()
})
}
diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts
new file mode 100644
index 000000000..4ec2737fb
--- /dev/null
+++ b/backend/src/server/routes/v1/secret-sharing-router.ts
@@ -0,0 +1,183 @@
+import { z } from "zod";
+
+import { SecretSharingSchema } from "@app/db/schemas";
+import {
+ publicEndpointLimit,
+ publicSecretShareCreationLimit,
+ readLimit,
+ writeLimit
+} from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ response: {
+ 200: z.array(SecretSharingSchema)
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const sharedSecrets = await req.server.services.secretSharing.getSharedSecrets({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ orgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ return sharedSecrets;
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/public/:id",
+ config: {
+ rateLimit: publicEndpointLimit
+ },
+ schema: {
+ params: z.object({
+ id: z.string().uuid()
+ }),
+ querystring: z.object({
+ hashedHex: z.string()
+ }),
+ response: {
+ 200: SecretSharingSchema.pick({
+ encryptedValue: true,
+ iv: true,
+ tag: true,
+ expiresAt: true,
+ expiresAfterViews: true
+ })
+ }
+ },
+ handler: async (req) => {
+ const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
+ req.params.id,
+ req.query.hashedHex
+ );
+ if (!sharedSecret) return undefined;
+ return {
+ encryptedValue: sharedSecret.encryptedValue,
+ iv: sharedSecret.iv,
+ tag: sharedSecret.tag,
+ expiresAt: sharedSecret.expiresAt,
+ expiresAfterViews: sharedSecret.expiresAfterViews
+ };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/public",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ body: z.object({
+ encryptedValue: z.string(),
+ iv: z.string(),
+ tag: z.string(),
+ hashedHex: z.string(),
+ expiresAt: z.string(),
+ expiresAfterViews: z.number()
+ }),
+ response: {
+ 200: z.object({
+ id: z.string().uuid()
+ })
+ }
+ },
+ handler: async (req) => {
+ const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
+ const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
+ encryptedValue,
+ iv,
+ tag,
+ hashedHex,
+ expiresAt: new Date(expiresAt),
+ expiresAfterViews
+ });
+ return { id: sharedSecret.id };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/",
+ config: {
+ rateLimit: publicSecretShareCreationLimit
+ },
+ schema: {
+ body: z.object({
+ encryptedValue: z.string(),
+ iv: z.string(),
+ tag: z.string(),
+ hashedHex: z.string(),
+ expiresAt: z.string(),
+ expiresAfterViews: z.number()
+ }),
+ response: {
+ 200: z.object({
+ id: z.string().uuid()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
+ const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ orgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ encryptedValue,
+ iv,
+ tag,
+ hashedHex,
+ expiresAt: new Date(expiresAt),
+ expiresAfterViews
+ });
+ return { id: sharedSecret.id };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:sharedSecretId",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ sharedSecretId: z.string().uuid()
+ }),
+ response: {
+ 200: SecretSharingSchema
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const { sharedSecretId } = req.params;
+ const deletedSharedSecret = await req.server.services.secretSharing.deleteSharedSecretById({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ orgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ sharedSecretId
+ });
+
+ return { ...deletedSharedSecret };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts
index 1715aa3c3..ce92409f6 100644
--- a/backend/src/server/routes/v1/secret-tag-router.ts
+++ b/backend/src/server/routes/v1/secret-tag-router.ts
@@ -23,7 +23,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTags = await server.services.secretTag.getProjectTags({
actor: req.permission.type,
@@ -36,6 +36,67 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ method: "GET",
+ url: "/:projectId/tags/:tagId",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId),
+ tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId)
+ }),
+ response: {
+ 200: z.object({
+ workspaceTag: SecretTagsSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const workspaceTag = await server.services.secretTag.getTagById({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ id: req.params.tagId
+ });
+ return { workspaceTag };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:projectId/tags/slug/:tagSlug",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId),
+ tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug)
+ }),
+ response: {
+ 200: z.object({
+ workspaceTag: SecretTagsSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const workspaceTag = await server.services.secretTag.getTagBySlug({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ slug: req.params.tagSlug,
+ projectId: req.params.projectId
+ });
+ return { workspaceTag };
+ }
+ });
+
server.route({
method: "POST",
url: "/:projectId/tags",
@@ -57,7 +118,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.createTag({
actor: req.permission.type,
@@ -71,6 +132,42 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ method: "PATCH",
+ url: "/:projectId/tags/:tagId",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId),
+ tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
+ }),
+ body: z.object({
+ name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
+ slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
+ color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
+ }),
+ response: {
+ 200: z.object({
+ workspaceTag: SecretTagsSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const workspaceTag = await server.services.secretTag.updateTag({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body,
+ id: req.params.tagId
+ });
+ return { workspaceTag };
+ }
+ });
+
server.route({
method: "DELETE",
url: "/:projectId/tags/:tagId",
@@ -88,7 +185,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.deleteTag({
actor: req.permission.type,
diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts
index 60bbec7db..3d8d02e6f 100644
--- a/backend/src/server/routes/v1/sso-router.ts
+++ b/backend/src/server/routes/v1/sso-router.ts
@@ -259,4 +259,50 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
);
}
});
+
+ server.route({
+ url: "/token-exchange",
+ method: "POST",
+ schema: {
+ body: z.object({
+ providerAuthToken: z.string(),
+ email: z.string()
+ })
+ },
+ handler: async (req, res) => {
+ const userAgent = req.headers["user-agent"];
+ if (!userAgent) throw new Error("user agent header is required");
+
+ const data = await server.services.login.oauth2TokenExchange({
+ email: req.body.email,
+ ip: req.realIp,
+ userAgent,
+ providerAuthToken: req.body.providerAuthToken
+ });
+
+ if (data.isMfaEnabled) {
+ return { mfaEnabled: true, token: data.token } as const; // for discriminated union
+ }
+
+ void res.setCookie("jid", data.token.refresh, {
+ httpOnly: true,
+ path: "/",
+ sameSite: "strict",
+ secure: appCfg.HTTPS_ENABLED
+ });
+
+ return {
+ mfaEnabled: false,
+ encryptionVersion: data.user.encryptionVersion,
+ token: data.token.access,
+ publicKey: data.user.publicKey,
+ encryptedPrivateKey: data.user.encryptedPrivateKey,
+ iv: data.user.iv,
+ tag: data.user.tag,
+ protectedKey: data.user.protectedKey || null,
+ protectedKeyIV: data.user.protectedKeyIV || null,
+ protectedKeyTag: data.user.protectedKeyTag || null
+ } as const;
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts
index bdede8a3a..b9269d66e 100644
--- a/backend/src/server/routes/v1/user-router.ts
+++ b/backend/src/server/routes/v1/user-router.ts
@@ -1,11 +1,15 @@
import { z } from "zod";
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
-import { readLimit } from "@app/server/config/rateLimiter";
+import { getConfig } from "@app/lib/config/env";
+import { logger } from "@app/lib/logger";
+import { authRateLimit, readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => {
+ const appCfg = getConfig();
+
server.route({
method: "GET",
url: "/",
@@ -15,7 +19,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
200: z.object({
- user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
+ user: UsersSchema.merge(
+ UserEncryptionKeysSchema.pick({
+ clientPublicKey: true,
+ serverPrivateKey: true,
+ encryptionVersion: true,
+ protectedKey: true,
+ protectedKeyIV: true,
+ protectedKeyTag: true,
+ publicKey: true,
+ encryptedPrivateKey: true,
+ iv: true,
+ tag: true,
+ salt: true,
+ verifier: true,
+ userId: true
+ })
+ )
})
}
},
@@ -25,4 +45,49 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
return { user };
}
});
+
+ server.route({
+ method: "GET",
+ url: "/private-key",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ response: {
+ 200: z.object({
+ privateKey: z.string()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
+ handler: async (req) => {
+ const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
+ return { privateKey };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:userId/unlock",
+ config: {
+ rateLimit: authRateLimit
+ },
+ schema: {
+ querystring: z.object({
+ token: z.string().trim()
+ }),
+ params: z.object({
+ userId: z.string()
+ })
+ },
+ handler: async (req, res) => {
+ try {
+ await server.services.user.unlockUser(req.params.userId, req.query.token);
+ } catch (err) {
+ logger.error(`User unlock failed for ${req.params.userId}`);
+ logger.error(err);
+ }
+ return res.redirect(`${appCfg.SITE_URL}/login`);
+ }
+ });
};
diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts
index 973804c7c..1c685866d 100644
--- a/backend/src/server/routes/v2/mfa-router.ts
+++ b/backend/src/server/routes/v2/mfa-router.ts
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
-import { writeLimit } from "@app/server/config/rateLimiter";
+import { mfaRateLimit } from "@app/server/config/rateLimiter";
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
export const registerMfaRouter = async (server: FastifyZodProvider) => {
@@ -34,7 +34,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/mfa/send",
config: {
- rateLimit: writeLimit
+ rateLimit: mfaRateLimit
},
schema: {
response: {
@@ -53,7 +53,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
url: "/mfa/verify",
method: "POST",
config: {
- rateLimit: writeLimit
+ rateLimit: mfaRateLimit
},
schema: {
body: z.object({
diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts
index a199cf0d4..e1c7c2e69 100644
--- a/backend/src/server/routes/v2/project-router.ts
+++ b/backend/src/server/routes/v2/project-router.ts
@@ -1,13 +1,14 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
-import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
+import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
+import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { ProjectFilterType } from "@app/services/project/project-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -307,4 +308,80 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return project;
}
});
+
+ server.route({
+ method: "GET",
+ url: "/:slug/cas",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ slug: slugSchema.describe("The slug of the project to list CAs.")
+ }),
+ querystring: z.object({
+ status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional()
+ }),
+ response: {
+ 200: z.object({
+ cas: z.array(CertificateAuthoritiesSchema)
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const cas = await server.services.project.listProjectCas({
+ filter: {
+ slug: req.params.slug,
+ orgId: req.permission.orgId,
+ type: ProjectFilterType.SLUG
+ },
+ status: req.query.status,
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ actor: req.permission.type
+ });
+ return { cas };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:slug/certificates",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ slug: slugSchema.describe("The slug of the project to list certificates.")
+ }),
+ querystring: z.object({
+ offset: z.coerce.number().min(0).max(100).default(0),
+ limit: z.coerce.number().min(1).max(100).default(25)
+ }),
+ response: {
+ 200: z.object({
+ certificates: z.array(CertificatesSchema),
+ totalCount: z.number()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const { certificates, totalCount } = await server.services.project.listProjectCertificates({
+ filter: {
+ slug: req.params.slug,
+ orgId: req.permission.orgId,
+ type: ProjectFilterType.SLUG
+ },
+ actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ actor: req.permission.type,
+ ...req.query
+ });
+ return { certificates, totalCount };
+ }
+ });
};
diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts
index 1f15008c7..21dd32021 100644
--- a/backend/src/server/routes/v2/user-router.ts
+++ b/backend/src/server/routes/v2/user-router.ts
@@ -255,7 +255,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
description: "Retrieve the current user on the request",
response: {
200: z.object({
- user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
+ user: UsersSchema.merge(
+ UserEncryptionKeysSchema.pick({
+ clientPublicKey: true,
+ serverPrivateKey: true,
+ encryptionVersion: true,
+ protectedKey: true,
+ protectedKeyIV: true,
+ protectedKeyTag: true,
+ publicKey: true,
+ encryptedPrivateKey: true,
+ iv: true,
+ tag: true,
+ salt: true,
+ verifier: true,
+ userId: true
+ })
+ )
})
}
},
diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts
index 900ad56d2..61a0c74e5 100644
--- a/backend/src/server/routes/v3/login-router.ts
+++ b/backend/src/server/routes/v3/login-router.ts
@@ -80,7 +80,9 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
body: z.object({
email: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
- clientProof: z.string().trim()
+ clientProof: z.string().trim(),
+ captchaToken: z.string().trim().optional(),
+ password: z.string().optional()
}),
response: {
200: z.discriminatedUnion("mfaEnabled", [
@@ -106,11 +108,13 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const data = await server.services.login.loginExchangeClientProof({
+ captchaToken: req.body.captchaToken,
email: req.body.email,
ip: req.realIp,
userAgent,
providerAuthToken: req.body.providerAuthToken,
- clientProof: req.body.clientProof
+ clientProof: req.body.clientProof,
+ password: req.body.password
});
if (data.isMfaEnabled) {
diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts
index 6fa574a69..e3f1528ad 100644
--- a/backend/src/server/routes/v3/secret-router.ts
+++ b/backend/src/server/routes/v3/secret-router.ts
@@ -8,8 +8,7 @@ import {
SecretType,
ServiceTokenScopes
} from "@app/db/schemas";
-import { EventType } from "@app/ee/services/audit-log/audit-log-types";
-import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
+import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
@@ -19,6 +18,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
+import { SecretOperations } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
@@ -259,18 +259,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
- await server.services.telemetry.sendPostHogEvents({
- event: PostHogEventTypes.SecretPulled,
- distinctId: getTelemetryDistinctId(req),
- properties: {
- numberOfSecrets: secrets.length,
- workspaceId,
- environment,
- secretPath: req.query.secretPath,
- channel: getUserAgentType(req.headers["user-agent"]),
- ...req.auditLogInfo
- }
- });
+ if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
+ await server.services.telemetry.sendPostHogEvents({
+ event: PostHogEventTypes.SecretPulled,
+ distinctId: getTelemetryDistinctId(req),
+ properties: {
+ numberOfSecrets: secrets.length,
+ workspaceId,
+ environment,
+ secretPath: req.query.secretPath,
+ channel: getUserAgentType(req.headers["user-agent"]),
+ ...req.auditLogInfo
+ }
+ });
+ }
return { secrets, imports };
}
});
@@ -306,7 +308,16 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
- secret: secretRawSchema
+ secret: secretRawSchema.extend({
+ tags: SecretTagsSchema.pick({
+ id: true,
+ slug: true,
+ name: true,
+ color: true
+ })
+ .array()
+ .optional()
+ })
})
}
},
@@ -358,18 +369,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
- await server.services.telemetry.sendPostHogEvents({
- event: PostHogEventTypes.SecretPulled,
- distinctId: getTelemetryDistinctId(req),
- properties: {
- numberOfSecrets: 1,
- workspaceId: secret.workspace,
- environment,
- secretPath: req.query.secretPath,
- channel: getUserAgentType(req.headers["user-agent"]),
- ...req.auditLogInfo
- }
- });
+ if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
+ await server.services.telemetry.sendPostHogEvents({
+ event: PostHogEventTypes.SecretPulled,
+ distinctId: getTelemetryDistinctId(req),
+ properties: {
+ numberOfSecrets: 1,
+ workspaceId: secret.workspace,
+ environment,
+ secretPath: req.query.secretPath,
+ channel: getUserAgentType(req.headers["user-agent"]),
+ ...req.auditLogInfo
+ }
+ });
+ }
return { secret };
}
});
@@ -404,6 +417,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.CREATE.secretValue),
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
+ tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type)
}),
@@ -427,7 +441,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
type: req.body.type,
secretValue: req.body.secretValue,
skipMultilineEncoding: req.body.skipMultilineEncoding,
- secretComment: req.body.secretComment
+ secretComment: req.body.secretComment,
+ tagIds: req.body.tagIds
});
await server.services.auditLog.createAuditLog({
@@ -492,7 +507,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.UPDATE.secretPath),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
- type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type)
+ type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
+ tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds)
}),
response: {
200: z.object({
@@ -513,7 +529,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: req.params.secretName,
type: req.body.type,
secretValue: req.body.secretValue,
- skipMultilineEncoding: req.body.skipMultilineEncoding
+ skipMultilineEncoding: req.body.skipMultilineEncoding,
+ tagIds: req.body.tagIds
});
await server.services.auditLog.createAuditLog({
@@ -710,24 +727,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
// TODO: Move to telemetry plugin
- let shouldRecordK8Event = false;
- if (req.headers["user-agent"] === "k8-operatoer") {
- const randomNumber = Math.random();
- if (randomNumber > 0.95) {
- shouldRecordK8Event = true;
- }
- }
+ // let shouldRecordK8Event = false;
+ // if (req.headers["user-agent"] === "k8-operatoer") {
+ // const randomNumber = Math.random();
+ // if (randomNumber > 0.95) {
+ // shouldRecordK8Event = true;
+ // }
+ // }
const shouldCapture =
- req.query.workspaceId !== "650e71fbae3e6c8572f436d4" &&
- (req.headers["user-agent"] !== "k8-operator" || shouldRecordK8Event);
- const approximateNumberTotalSecrets = secrets.length * 20;
+ req.query.workspaceId !== "650e71fbae3e6c8572f436d4" && req.headers["user-agent"] !== "k8-operator";
if (shouldCapture) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
- numberOfSecrets: shouldRecordK8Event ? approximateNumberTotalSecrets : secrets.length,
+ numberOfSecrets: secrets.length,
workspaceId: req.query.workspaceId,
environment: req.query.environment,
secretPath: req.query.secretPath,
@@ -804,18 +819,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
- await server.services.telemetry.sendPostHogEvents({
- event: PostHogEventTypes.SecretPulled,
- distinctId: getTelemetryDistinctId(req),
- properties: {
- numberOfSecrets: 1,
- workspaceId: req.query.workspaceId,
- environment: req.query.environment,
- secretPath: req.query.secretPath,
- channel: getUserAgentType(req.headers["user-agent"]),
- ...req.auditLogInfo
- }
- });
+ if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
+ await server.services.telemetry.sendPostHogEvents({
+ event: PostHogEventTypes.SecretPulled,
+ distinctId: getTelemetryDistinctId(req),
+ properties: {
+ numberOfSecrets: 1,
+ workspaceId: req.query.workspaceId,
+ environment: req.query.environment,
+ secretPath: req.query.secretPath,
+ channel: getUserAgentType(req.headers["user-agent"]),
+ ...req.auditLogInfo
+ }
+ });
+ }
return { secret };
}
});
@@ -902,7 +919,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId,
policy,
data: {
- [CommitType.Create]: [
+ [SecretOperations.Create]: [
{
secretName: req.params.secretName,
secretValueCiphertext,
@@ -1084,7 +1101,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId,
policy,
data: {
- [CommitType.Update]: [
+ [SecretOperations.Update]: [
{
secretName: req.params.secretName,
newSecretName,
@@ -1234,7 +1251,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId,
policy,
data: {
- [CommitType.Delete]: [
+ [SecretOperations.Delete]: [
{
secretName: req.params.secretName
}
@@ -1364,7 +1381,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId,
policy,
data: {
- [CommitType.Create]: inputSecrets
+ [SecretOperations.Create]: inputSecrets
}
});
@@ -1491,7 +1508,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId,
policy,
data: {
- [CommitType.Update]: inputSecrets.filter(({ type }) => type === "shared")
+ [SecretOperations.Update]: inputSecrets.filter(({ type }) => type === "shared")
}
});
@@ -1606,7 +1623,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId,
policy,
data: {
- [CommitType.Delete]: inputSecrets.filter(({ type }) => type === "shared")
+ [SecretOperations.Delete]: inputSecrets.filter(({ type }) => type === "shared")
}
});
await server.services.auditLog.createAuditLog({
diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts
index ac43df36d..59131464a 100644
--- a/backend/src/server/routes/v3/signup-router.ts
+++ b/backend/src/server/routes/v3/signup-router.ts
@@ -102,7 +102,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
verifier: z.string().trim(),
organizationName: z.string().trim().min(1),
providerAuthToken: z.string().trim().optional().nullish(),
- attributionSource: z.string().trim().optional()
+ attributionSource: z.string().trim().optional(),
+ password: z.string()
}),
response: {
200: z.object({
@@ -167,6 +168,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
email: z.string().email().trim(),
+ password: z.string(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts
index 5d68a4e94..b1f8aa2f6 100644
--- a/backend/src/services/auth-token/auth-token-service.ts
+++ b/backend/src/services/auth-token/auth-token-service.ts
@@ -13,8 +13,9 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
type TAuthTokenServiceFactoryDep = {
tokenDAL: TTokenDALFactory;
- userDAL: Pick;
+ userDAL: Pick;
};
+
export type TAuthTokenServiceFactory = ReturnType;
export const getTokenConfig = (tokenType: TokenType) => {
@@ -53,6 +54,11 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
+ case TokenType.TOKEN_USER_UNLOCK: {
+ const token = crypto.randomBytes(16).toString("hex");
+ const expiresAt = new Date(new Date().getTime() + 259200000);
+ return { token, expiresAt };
+ }
default: {
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date();
diff --git a/backend/src/services/auth-token/auth-token-types.ts b/backend/src/services/auth-token/auth-token-types.ts
index 630e36310..8917bd672 100644
--- a/backend/src/services/auth-token/auth-token-types.ts
+++ b/backend/src/services/auth-token/auth-token-types.ts
@@ -3,7 +3,8 @@ export enum TokenType {
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
TOKEN_EMAIL_MFA = "emailMfa",
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
- TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
+ TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
+ TOKEN_USER_UNLOCK = "userUnlock"
}
export type TCreateTokenForUserDTO = {
diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts
index 80fb0b325..e8574a8b9 100644
--- a/backend/src/services/auth/auth-fns.ts
+++ b/backend/src/services/auth/auth-fns.ts
@@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
if (decodedToken.organizationId) {
- return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod };
+ return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod, userName: decodedToken.username };
}
- return { authMethod: decodedToken.authMethod, orgId: null };
+ return { authMethod: decodedToken.authMethod, orgId: null, userName: decodedToken.username };
};
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
@@ -44,3 +44,27 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
if (decodedToken.userId !== userId) throw new UnauthorizedError();
};
+
+export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
+ if (isLocked) {
+ throw new UnauthorizedError({
+ name: "User Locked",
+ message:
+ "User is locked due to multiple failed login attempts. An email has been sent to you in order to unlock your account. You can also reset your password to unlock your account."
+ });
+ }
+
+ if (temporaryLockDateEnd) {
+ const timeDiff = new Date().getTime() - temporaryLockDateEnd.getTime();
+ if (timeDiff < 0) {
+ const secondsDiff = (-1 * timeDiff) / 1000;
+ const timeDisplay =
+ secondsDiff > 60 ? `${Math.ceil(secondsDiff / 60)} minutes` : `${Math.ceil(secondsDiff)} seconds`;
+
+ throw new UnauthorizedError({
+ name: "User Locked",
+ message: `User is temporary locked due to multiple failed login attempts. Try again after ${timeDisplay}. You can also reset your password now to proceed.`
+ });
+ }
+ }
+};
diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts
index 4d2a302c6..29a2a176f 100644
--- a/backend/src/services/auth/auth-login-service.ts
+++ b/backend/src/services/auth/auth-login-service.ts
@@ -1,10 +1,14 @@
+import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
+import { request } from "@app/lib/config/request";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
-import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
+import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { getUserPrivateKey } from "@app/lib/crypto/srp";
+import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
@@ -13,11 +17,12 @@ import { TokenType } from "../auth-token/auth-token-types";
import { TOrgDALFactory } from "../org/org-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
-import { validateProviderAuthToken } from "./auth-fns";
+import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
import {
TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO,
TOauthLoginDTO,
+ TOauthTokenExchangeDTO,
TVerifyMfaTokenDTO
} from "./auth-login-type";
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
@@ -100,7 +105,7 @@ export const authLoginServiceFactory = ({
user: TUsers;
ip: string;
userAgent: string;
- organizationId: string | undefined;
+ organizationId?: string;
authMethod: AuthMethod;
}) => {
const cfg = getConfig();
@@ -176,12 +181,17 @@ export const authLoginServiceFactory = ({
clientProof,
ip,
userAgent,
- providerAuthToken
+ providerAuthToken,
+ captchaToken,
+ password
}: TLoginClientProofDTO) => {
+ const appCfg = getConfig();
+
const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc) throw new Error("Failed to find user");
+ const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig();
let authMethod = AuthMethod.EMAIL;
@@ -196,6 +206,31 @@ export const authLoginServiceFactory = ({
}
}
+ if (
+ user.consecutiveFailedPasswordAttempts &&
+ user.consecutiveFailedPasswordAttempts >= 10 &&
+ Boolean(appCfg.CAPTCHA_SECRET)
+ ) {
+ if (!captchaToken) {
+ throw new BadRequestError({
+ name: "Captcha Required",
+ message: "Accomplish the required captcha by logging in via Web"
+ });
+ }
+
+ // validate captcha token
+ const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
+ response: captchaToken,
+ secret: appCfg.CAPTCHA_SECRET
+ });
+
+ if (!response.data.success) {
+ throw new BadRequestError({
+ name: "Invalid Captcha"
+ });
+ }
+ }
+
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
@@ -204,14 +239,48 @@ export const authLoginServiceFactory = ({
userEnc.clientPublicKey,
clientProof
);
- if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
- await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
- serverPrivateKey: null,
- clientPublicKey: null
+ if (!isValidClientProof) {
+ await userDAL.update(
+ { id: userEnc.userId },
+ {
+ $incr: {
+ consecutiveFailedPasswordAttempts: 1
+ }
+ }
+ );
+
+ throw new Error("Failed to authenticate. Try again?");
+ }
+
+ await userDAL.updateById(userEnc.userId, {
+ consecutiveFailedPasswordAttempts: 0
});
+ // from password decrypt the private key
+ if (password) {
+ const privateKey = await getUserPrivateKey(password, userEnc);
+ const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
+ const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
+ await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
+ serverPrivateKey: null,
+ clientPublicKey: null,
+ hashedPassword,
+ serverEncryptedPrivateKey: ciphertext,
+ serverEncryptedPrivateKeyIV: iv,
+ serverEncryptedPrivateKeyTag: tag,
+ serverEncryptedPrivateKeyEncoding: encoding
+ });
+ } else {
+ await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
+ serverPrivateKey: null,
+ clientPublicKey: null
+ });
+ }
+
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) {
+ enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
+
const mfaToken = jwt.sign(
{
authMethod,
@@ -300,28 +369,111 @@ export const authLoginServiceFactory = ({
const resendMfaToken = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user || !user.email) return;
+ enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
await sendUserMfaCode({
userId: user.id,
email: user.email
});
};
+ const processFailedMfaAttempt = async (userId: string) => {
+ try {
+ const updatedUser = await userDAL.transaction(async (tx) => {
+ const PROGRESSIVE_DELAY_INTERVAL = 3;
+ const user = await userDAL.updateById(userId, { $incr: { consecutiveFailedMfaAttempts: 1 } }, tx);
+
+ if (!user) {
+ throw new Error("User not found");
+ }
+
+ const progressiveDelaysInMins = [5, 30, 60];
+
+ // lock user when failed attempt exceeds threshold
+ if (
+ user.consecutiveFailedMfaAttempts &&
+ user.consecutiveFailedMfaAttempts >= PROGRESSIVE_DELAY_INTERVAL * (progressiveDelaysInMins.length + 1)
+ ) {
+ return userDAL.updateById(
+ userId,
+ {
+ isLocked: true,
+ temporaryLockDateEnd: null
+ },
+ tx
+ );
+ }
+
+ // delay user only when failed MFA attempts is a multiple of configured delay interval
+ if (user.consecutiveFailedMfaAttempts && user.consecutiveFailedMfaAttempts % PROGRESSIVE_DELAY_INTERVAL === 0) {
+ const delayIndex = user.consecutiveFailedMfaAttempts / PROGRESSIVE_DELAY_INTERVAL - 1;
+ return userDAL.updateById(
+ userId,
+ {
+ temporaryLockDateEnd: new Date(new Date().getTime() + progressiveDelaysInMins[delayIndex] * 60 * 1000)
+ },
+ tx
+ );
+ }
+
+ return user;
+ });
+
+ return updatedUser;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Process failed MFA Attempt" });
+ }
+ };
+
/*
* Multi factor authentication verification of code
* Third step of login in which user completes with mfa
* */
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
- await tokenService.validateTokenForUser({
- type: TokenType.TOKEN_EMAIL_MFA,
- userId,
- code: mfaToken
- });
+ const appCfg = getConfig();
+ const user = await userDAL.findById(userId);
+ enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
+
+ try {
+ await tokenService.validateTokenForUser({
+ type: TokenType.TOKEN_EMAIL_MFA,
+ userId,
+ code: mfaToken
+ });
+ } catch (err) {
+ const updatedUser = await processFailedMfaAttempt(userId);
+ if (updatedUser.isLocked) {
+ if (updatedUser.email) {
+ const unlockToken = await tokenService.createTokenForUser({
+ type: TokenType.TOKEN_USER_UNLOCK,
+ userId: updatedUser.id
+ });
+
+ await smtpService.sendMail({
+ template: SmtpTemplates.UnlockAccount,
+ subjectLine: "Unlock your Infisical account",
+ recipients: [updatedUser.email],
+ substitutions: {
+ token: unlockToken,
+ callback_url: `${appCfg.SITE_URL}/api/v1/user/${updatedUser.id}/unlock`
+ }
+ });
+ }
+ }
+
+ throw err;
+ }
const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to authenticate user");
+ // reset lock states
+ await userDAL.updateById(userId, {
+ consecutiveFailedMfaAttempts: 0,
+ temporaryLockDateEnd: null
+ });
+
const token = await generateUserTokens({
user: {
...userEnc,
@@ -367,8 +519,14 @@ export const authLoginServiceFactory = ({
authMethods: [authMethod],
isGhost: false
});
+ } else {
+ const isLinkingRequired = !user?.authMethods?.includes(authMethod);
+ if (isLinkingRequired) {
+ user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] });
+ }
}
- const isLinkingRequired = !user?.authMethods?.includes(authMethod);
+
+ const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const isUserCompleted = user.isAccepted;
const providerAuthToken = jwt.sign(
{
@@ -379,9 +537,9 @@ export const authLoginServiceFactory = ({
isEmailVerified: user.isEmailVerified,
firstName: user.firstName,
lastName: user.lastName,
+ hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
authMethod,
isUserCompleted,
- isLinkingRequired,
...(callbackPort
? {
callbackPort
@@ -393,10 +551,71 @@ export const authLoginServiceFactory = ({
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
}
);
-
return { isUserCompleted, providerAuthToken };
};
+ /**
+ * Handles OAuth2 token exchange for user login with private key handoff.
+ *
+ * The process involves exchanging a provider's authorization token for an Infisical access token.
+ * The provider token is returned to the client, who then sends it back to obtain the Infisical access token.
+ *
+ * This approach is used instead of directly sending the access token for the following reasons:
+ * 1. To facilitate easier logic changes from SRP OAuth to simple OAuth.
+ * 2. To avoid attaching the access token to the URL, which could be logged. The provider token has a very short lifespan, reducing security risks.
+ */
+ const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => {
+ const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
+
+ const appCfg = getConfig();
+ const { authMethod, userName } = decodedProviderToken;
+ if (!userName) throw new BadRequestError({ message: "Missing user name" });
+ const organizationId =
+ (isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId
+ ? decodedProviderToken.orgId
+ : undefined;
+
+ const userEnc = await userDAL.findUserEncKeyByUsername({
+ username: email
+ });
+ if (!userEnc) throw new BadRequestError({ message: "Invalid token" });
+ if (!userEnc.serverEncryptedPrivateKey)
+ throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
+ // send multi factor auth token if they it enabled
+ if (userEnc.isMfaEnabled && userEnc.email) {
+ enforceUserLockStatus(Boolean(userEnc.isLocked), userEnc.temporaryLockDateEnd);
+
+ const mfaToken = jwt.sign(
+ {
+ authMethod,
+ authTokenType: AuthTokenType.MFA_TOKEN,
+ userId: userEnc.userId
+ },
+ appCfg.AUTH_SECRET,
+ {
+ expiresIn: appCfg.JWT_MFA_LIFETIME
+ }
+ );
+
+ await sendUserMfaCode({
+ userId: userEnc.userId,
+ email: userEnc.email
+ });
+
+ return { isMfaEnabled: true, token: mfaToken } as const;
+ }
+
+ const token = await generateUserTokens({
+ user: { ...userEnc, id: userEnc.userId },
+ ip,
+ userAgent,
+ authMethod,
+ organizationId
+ });
+
+ return { token, isMfaEnabled: false, user: userEnc } as const;
+ };
+
/*
* logout user by incrementing the version by 1 meaning any old session will become invalid
* as there number is behind
@@ -410,6 +629,7 @@ export const authLoginServiceFactory = ({
loginExchangeClientProof,
logout,
oauth2Login,
+ oauth2TokenExchange,
resendMfaToken,
verifyMfaToken,
selectOrganization,
diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts
index 37b90f548..db57d730e 100644
--- a/backend/src/services/auth/auth-login-type.ts
+++ b/backend/src/services/auth/auth-login-type.ts
@@ -12,6 +12,8 @@ export type TLoginClientProofDTO = {
providerAuthToken?: string;
ip: string;
userAgent: string;
+ captchaToken?: string;
+ password?: string;
};
export type TVerifyMfaTokenDTO = {
@@ -30,3 +32,10 @@ export type TOauthLoginDTO = {
authMethod: AuthMethod;
callbackPort?: string;
};
+
+export type TOauthTokenExchangeDTO = {
+ providerAuthToken: string;
+ ip: string;
+ userAgent: string;
+ email: string;
+};
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index 4025e4903..0e6558966 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -1,3 +1,4 @@
+import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
@@ -57,7 +58,8 @@ export const authPaswordServiceFactory = ({
encryptedPrivateKeyTag,
salt,
verifier,
- tokenVersionId
+ tokenVersionId,
+ password
}: TChangePasswordDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
@@ -76,6 +78,8 @@ export const authPaswordServiceFactory = ({
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
+ const appCfg = getConfig();
+ const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
@@ -87,7 +91,8 @@ export const authPaswordServiceFactory = ({
salt,
verifier,
serverPrivateKey: null,
- clientPublicKey: null
+ clientPublicKey: null,
+ hashedPassword
});
if (tokenVersionId) {
@@ -174,6 +179,12 @@ export const authPaswordServiceFactory = ({
salt,
verifier
});
+
+ await userDAL.updateById(userId, {
+ isLocked: false,
+ temporaryLockDateEnd: null,
+ consecutiveFailedMfaAttempts: 0
+ });
};
/*
diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts
index cf2aac08d..a52374506 100644
--- a/backend/src/services/auth/auth-password-type.ts
+++ b/backend/src/services/auth/auth-password-type.ts
@@ -10,6 +10,7 @@ export type TChangePasswordDTO = {
salt: string;
verifier: string;
tokenVersionId?: string;
+ password: string;
};
export type TResetPasswordViaBackupKeyDTO = {
diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts
index be7f5777d..8cf2c9d34 100644
--- a/backend/src/services/auth/auth-signup-service.ts
+++ b/backend/src/services/auth/auth-signup-service.ts
@@ -1,3 +1,4 @@
+import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
@@ -6,6 +7,8 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
+import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
@@ -119,6 +122,7 @@ export const authSignupServiceFactory = ({
const completeEmailAccountSignup = async ({
email,
+ password,
firstName,
lastName,
providerAuthToken,
@@ -137,6 +141,7 @@ export const authSignupServiceFactory = ({
userAgent,
authorization
}: TCompleteAccountSignupDTO) => {
+ const appCfg = getConfig();
const user = await userDAL.findOne({ username: email });
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
@@ -152,6 +157,17 @@ export const authSignupServiceFactory = ({
validateSignUpAuthorization(authorization, user.id);
}
+ const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
+ const privateKey = await getUserPrivateKey(password, {
+ salt,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ encryptedPrivateKey,
+ iv: encryptedPrivateKeyIV,
+ tag: encryptedPrivateKeyTag
+ });
+ const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
@@ -166,7 +182,12 @@ export const authSignupServiceFactory = ({
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
- tag: encryptedPrivateKeyTag
+ tag: encryptedPrivateKeyTag,
+ hashedPassword,
+ serverEncryptedPrivateKeyEncoding: encoding,
+ serverEncryptedPrivateKeyTag: tag,
+ serverEncryptedPrivateKeyIV: iv,
+ serverEncryptedPrivateKey: ciphertext
},
tx
);
@@ -227,11 +248,10 @@ export const authSignupServiceFactory = ({
userId: updateduser.info.id
});
if (!tokenSession) throw new Error("Failed to create token");
- const appCfg = getConfig();
const accessToken = jwt.sign(
{
- authMethod: AuthMethod.EMAIL,
+ authMethod: authMethod || AuthMethod.EMAIL,
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
@@ -244,7 +264,7 @@ export const authSignupServiceFactory = ({
const refreshToken = jwt.sign(
{
- authMethod: AuthMethod.EMAIL,
+ authMethod: authMethod || AuthMethod.EMAIL,
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
@@ -265,6 +285,7 @@ export const authSignupServiceFactory = ({
ip,
salt,
email,
+ password,
verifier,
firstName,
publicKey,
@@ -295,6 +316,18 @@ export const authSignupServiceFactory = ({
name: "complete account invite"
});
+ const appCfg = getConfig();
+ const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
+ const privateKey = await getUserPrivateKey(password, {
+ salt,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ encryptedPrivateKey,
+ iv: encryptedPrivateKeyIV,
+ tag: encryptedPrivateKeyTag
+ });
+ const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
@@ -310,7 +343,12 @@ export const authSignupServiceFactory = ({
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
- tag: encryptedPrivateKeyTag
+ tag: encryptedPrivateKeyTag,
+ hashedPassword,
+ serverEncryptedPrivateKeyEncoding: encoding,
+ serverEncryptedPrivateKeyTag: tag,
+ serverEncryptedPrivateKeyIV: iv,
+ serverEncryptedPrivateKey: ciphertext
},
tx
);
@@ -343,7 +381,6 @@ export const authSignupServiceFactory = ({
userId: updateduser.info.id
});
if (!tokenSession) throw new Error("Failed to create token");
- const appCfg = getConfig();
const accessToken = jwt.sign(
{
diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts
index a37a1cd96..9cd70f8c7 100644
--- a/backend/src/services/auth/auth-signup-type.ts
+++ b/backend/src/services/auth/auth-signup-type.ts
@@ -1,5 +1,6 @@
export type TCompleteAccountSignupDTO = {
email: string;
+ password: string;
firstName: string;
lastName?: string;
protectedKey: string;
@@ -21,6 +22,7 @@ export type TCompleteAccountSignupDTO = {
export type TCompleteAccountInviteDTO = {
email: string;
+ password: string;
firstName: string;
lastName?: string;
protectedKey: string;
diff --git a/backend/src/services/certificate-authority/certificate-authority-cert-dal.ts b/backend/src/services/certificate-authority/certificate-authority-cert-dal.ts
new file mode 100644
index 000000000..763240986
--- /dev/null
+++ b/backend/src/services/certificate-authority/certificate-authority-cert-dal.ts
@@ -0,0 +1,10 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TCertificateAuthorityCertDALFactory = ReturnType;
+
+export const certificateAuthorityCertDALFactory = (db: TDbClient) => {
+ const caCertOrm = ormify(db, TableName.CertificateAuthorityCert);
+ return caCertOrm;
+};
diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts
new file mode 100644
index 000000000..1b4b30e73
--- /dev/null
+++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts
@@ -0,0 +1,48 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
+import { ormify } from "@app/lib/knex";
+
+export type TCertificateAuthorityDALFactory = ReturnType;
+
+export const certificateAuthorityDALFactory = (db: TDbClient) => {
+ const caOrm = ormify(db, TableName.CertificateAuthority);
+
+ // note: not used
+ const buildCertificateChain = async (caId: string) => {
+ try {
+ const result: {
+ caId: string;
+ parentCaId?: string;
+ encryptedCertificate: Buffer;
+ }[] = await db
+ .withRecursive("cte", (cte) => {
+ void cte
+ .select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate")
+ .from({ ca: TableName.CertificateAuthority })
+ .leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
+ .where("ca.id", caId)
+ .unionAll((builder) => {
+ void builder
+ .select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate")
+ .from({ ca: TableName.CertificateAuthority })
+ .leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
+ .innerJoin("cte", "cte.parentCaId", "ca.id");
+ });
+ })
+ .select("*")
+ .from("cte");
+
+ // Extract certificates and reverse the order to have the root CA at the end
+ const certChain: Buffer[] = result.map((row) => row.encryptedCertificate);
+ return certChain;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "BuildCertificateChain" });
+ }
+ };
+
+ return {
+ ...caOrm,
+ buildCertificateChain
+ };
+};
diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts
new file mode 100644
index 000000000..cf42a058e
--- /dev/null
+++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts
@@ -0,0 +1,216 @@
+import * as x509 from "@peculiar/x509";
+import crypto from "crypto";
+
+import { BadRequestError } from "@app/lib/errors";
+import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
+
+import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
+import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types";
+
+export const createDistinguishedName = (parts: TDNParts) => {
+ const dnParts = [];
+ if (parts.country) dnParts.push(`C=${parts.country}`);
+ if (parts.organization) dnParts.push(`O=${parts.organization}`);
+ if (parts.ou) dnParts.push(`OU=${parts.ou}`);
+ if (parts.province) dnParts.push(`ST=${parts.province}`);
+ if (parts.commonName) dnParts.push(`CN=${parts.commonName}`);
+ if (parts.locality) dnParts.push(`L=${parts.locality}`);
+ return dnParts.join(", ");
+};
+
+export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
+ switch (keyAlgorithm) {
+ case CertKeyAlgorithm.RSA_4096:
+ return {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ publicExponent: new Uint8Array([1, 0, 1]),
+ modulusLength: 4096
+ };
+ case CertKeyAlgorithm.ECDSA_P256:
+ return {
+ name: "ECDSA",
+ namedCurve: "P-256",
+ hash: "SHA-256"
+ };
+ case CertKeyAlgorithm.ECDSA_P384:
+ return {
+ name: "ECDSA",
+ namedCurve: "P-384",
+ hash: "SHA-384"
+ };
+ default: {
+ // RSA_2048
+ return {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ publicExponent: new Uint8Array([1, 0, 1]),
+ modulusLength: 2048
+ };
+ }
+ }
+};
+
+/**
+ * Return the public and private key of CA with id [caId]
+ * Note: credentials are returned as crypto.webcrypto.CryptoKey
+ * suitable for use with @peculiar/x509 module
+ */
+export const getCaCredentials = async ({
+ caId,
+ certificateAuthorityDAL,
+ certificateAuthoritySecretDAL,
+ projectDAL,
+ kmsService
+}: TGetCaCredentialsDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
+ if (!caSecret) throw new BadRequestError({ message: "CA secret not found" });
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const decryptedPrivateKey = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caSecret.encryptedPrivateKey
+ });
+
+ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
+ const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
+ const caPrivateKey = await crypto.subtle.importKey(
+ "pkcs8",
+ skObj.export({ format: "der", type: "pkcs8" }),
+ alg,
+ true,
+ ["sign"]
+ );
+
+ const pkObj = crypto.createPublicKey(skObj);
+ const caPublicKey = await crypto.subtle.importKey("spki", pkObj.export({ format: "der", type: "spki" }), alg, true, [
+ "verify"
+ ]);
+
+ return {
+ caPrivateKey,
+ caPublicKey
+ };
+};
+
+/**
+ * Return the decrypted pem-encoded certificate and certificate chain
+ * for CA with id [caId].
+ */
+export const getCaCertChain = async ({
+ caId,
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ projectDAL,
+ kmsService
+}: TGetCaCertChainDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const decryptedCaCert = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caCert.encryptedCertificate
+ });
+
+ const caCertObj = new x509.X509Certificate(decryptedCaCert);
+
+ const decryptedChain = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caCert.encryptedCertificateChain
+ });
+
+ return {
+ caCert: caCertObj.toString("pem"),
+ caCertChain: decryptedChain.toString("utf-8"),
+ serialNumber: caCertObj.serialNumber
+ };
+};
+
+/**
+ * Rebuilds the certificate revocation list (CRL)
+ * for CA with id [caId]
+ */
+export const rebuildCaCrl = async ({
+ caId,
+ certificateAuthorityDAL,
+ certificateAuthorityCrlDAL,
+ certificateAuthoritySecretDAL,
+ projectDAL,
+ certificateDAL,
+ kmsService
+}: TRebuildCaCrlDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
+
+ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const privateKey = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caSecret.encryptedPrivateKey
+ });
+
+ const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
+ const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
+ "sign"
+ ]);
+
+ const revokedCerts = await certificateDAL.find({
+ caId: ca.id,
+ status: CertStatus.REVOKED
+ });
+
+ const crl = await x509.X509CrlGenerator.create({
+ issuer: ca.dn,
+ thisUpdate: new Date(),
+ nextUpdate: new Date("2025/12/12"),
+ entries: revokedCerts.map((revokedCert) => {
+ return {
+ serialNumber: revokedCert.serialNumber,
+ revocationDate: new Date(revokedCert.revokedAt as Date),
+ reason: revokedCert.revocationReason as number,
+ invalidity: new Date("2022/01/01"),
+ issuer: ca.dn
+ };
+ }),
+ signingAlgorithm: alg,
+ signingKey: sk
+ });
+
+ const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.from(new Uint8Array(crl.rawData))
+ });
+
+ await certificateAuthorityCrlDAL.update(
+ {
+ caId: ca.id
+ },
+ {
+ encryptedCrl
+ }
+ );
+};
diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts
new file mode 100644
index 000000000..384f45c09
--- /dev/null
+++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts
@@ -0,0 +1,145 @@
+import * as x509 from "@peculiar/x509";
+import crypto from "crypto";
+
+import { getConfig } from "@app/lib/config/env";
+import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
+import { BadRequestError } from "@app/lib/errors";
+import { logger } from "@app/lib/logger";
+import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
+import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
+import { CertKeyAlgorithm, CertStatus } from "@app/services/certificate/certificate-types";
+import { TKmsServiceFactory } from "@app/services/kms/kms-service";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
+import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
+
+import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
+import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
+import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
+import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
+import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types";
+
+type TCertificateAuthorityQueueFactoryDep = {
+ // TODO: Pick
+ certificateAuthorityDAL: TCertificateAuthorityDALFactory;
+ certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;
+ certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory;
+ certificateDAL: TCertificateDALFactory;
+ projectDAL: Pick;
+ kmsService: Pick;
+ queueService: TQueueServiceFactory;
+};
+export type TCertificateAuthorityQueueFactory = ReturnType;
+
+export const certificateAuthorityQueueFactory = ({
+ certificateAuthorityCrlDAL,
+ certificateAuthorityDAL,
+ certificateAuthoritySecretDAL,
+ certificateDAL,
+ projectDAL,
+ kmsService,
+ queueService
+}: TCertificateAuthorityQueueFactoryDep) => {
+ // TODO 1: auto-periodic rotation
+ // TODO 2: manual rotation
+
+ const setCaCrlRotationInterval = async ({ caId, rotationIntervalDays }: TRotateCaCrlTriggerDTO) => {
+ const appCfg = getConfig();
+
+ // query for config
+ // const caCrl = await certificateAuthorityCrlDAL.findOne({
+ // caId
+ // });
+
+ await queueService.queue(
+ // TODO: clarify queue + job naming
+ QueueName.CaCrlRotation,
+ QueueJobs.CaCrlRotation,
+ {
+ caId
+ },
+ {
+ jobId: `ca-crl-rotation-${caId}`,
+ repeat: {
+ // on prod it this will be in days, in development this will be second
+ every:
+ appCfg.NODE_ENV === "development"
+ ? secondsToMillis(rotationIntervalDays)
+ : daysToMillisecond(rotationIntervalDays),
+ immediately: true
+ }
+ }
+ );
+ };
+
+ queueService.start(QueueName.CaCrlRotation, async (job) => {
+ const { caId } = job.data;
+ logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`);
+
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
+
+ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const privateKey = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caSecret.encryptedPrivateKey
+ });
+
+ const skObj = crypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
+ const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
+ "sign"
+ ]);
+
+ const revokedCerts = await certificateDAL.find({
+ caId: ca.id,
+ status: CertStatus.REVOKED
+ });
+
+ const crl = await x509.X509CrlGenerator.create({
+ issuer: ca.dn,
+ thisUpdate: new Date(),
+ nextUpdate: new Date("2025/12/12"), // TODO: depends on configured rebuild interval
+ entries: revokedCerts.map((revokedCert) => {
+ return {
+ serialNumber: revokedCert.serialNumber,
+ revocationDate: new Date(revokedCert.revokedAt as Date),
+ reason: revokedCert.revocationReason as number,
+ invalidity: new Date("2022/01/01"),
+ issuer: ca.dn
+ };
+ }),
+ signingAlgorithm: alg,
+ signingKey: sk
+ });
+
+ const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.from(new Uint8Array(crl.rawData))
+ });
+
+ await certificateAuthorityCrlDAL.update(
+ {
+ caId: ca.id
+ },
+ {
+ encryptedCrl
+ }
+ );
+ });
+
+ queueService.listen(QueueName.CaCrlRotation, "failed", (job, err) => {
+ logger.error(err, "Failed to rotate CA CRL %s", job?.id);
+ });
+
+ return {
+ setCaCrlRotationInterval
+ };
+};
diff --git a/backend/src/services/certificate-authority/certificate-authority-secret-dal.ts b/backend/src/services/certificate-authority/certificate-authority-secret-dal.ts
new file mode 100644
index 000000000..2ade72e7e
--- /dev/null
+++ b/backend/src/services/certificate-authority/certificate-authority-secret-dal.ts
@@ -0,0 +1,10 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TCertificateAuthoritySecretDALFactory = ReturnType;
+
+export const certificateAuthoritySecretDALFactory = (db: TDbClient) => {
+ const caSecretOrm = ormify(db, TableName.CertificateAuthoritySecret);
+ return caSecretOrm;
+};
diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts
new file mode 100644
index 000000000..2345180a3
--- /dev/null
+++ b/backend/src/services/certificate-authority/certificate-authority-service.ts
@@ -0,0 +1,821 @@
+/* eslint-disable no-bitwise */
+import { ForbiddenError } from "@casl/ability";
+import * as x509 from "@peculiar/x509";
+import crypto, { KeyObject } from "crypto";
+import ms from "ms";
+
+import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import { BadRequestError } from "@app/lib/errors";
+import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
+import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
+import { TKmsServiceFactory } from "@app/services/kms/kms-service";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
+import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
+
+import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
+import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
+import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
+import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
+import {
+ createDistinguishedName,
+ getCaCertChain,
+ getCaCredentials,
+ keyAlgorithmToAlgCfg
+} from "./certificate-authority-fns";
+import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
+import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
+import {
+ CaStatus,
+ CaType,
+ TCreateCaDTO,
+ TDeleteCaDTO,
+ TGetCaCertDTO,
+ TGetCaCsrDTO,
+ TGetCaDTO,
+ TImportCertToCaDTO,
+ TIssueCertFromCaDTO,
+ TSignIntermediateDTO,
+ TUpdateCaDTO
+} from "./certificate-authority-types";
+
+type TCertificateAuthorityServiceFactoryDep = {
+ certificateAuthorityDAL: Pick<
+ TCertificateAuthorityDALFactory,
+ "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
+ >;
+ certificateAuthorityCertDAL: Pick;
+ certificateAuthoritySecretDAL: Pick;
+ certificateAuthorityCrlDAL: Pick;
+ certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
+ certificateDAL: Pick;
+ certificateBodyDAL: Pick;
+ projectDAL: Pick;
+ kmsService: Pick;
+ permissionService: Pick;
+};
+
+export type TCertificateAuthorityServiceFactory = ReturnType;
+
+export const certificateAuthorityServiceFactory = ({
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ certificateAuthoritySecretDAL,
+ certificateAuthorityCrlDAL,
+ certificateDAL,
+ certificateBodyDAL,
+ projectDAL,
+ kmsService,
+ permissionService
+}: TCertificateAuthorityServiceFactoryDep) => {
+ /**
+ * Generates new root or intermediate CA
+ */
+ const createCa = async ({
+ projectSlug,
+ type,
+ friendlyName,
+ commonName,
+ organization,
+ ou,
+ country,
+ province,
+ locality,
+ notBefore,
+ notAfter,
+ maxPathLength,
+ keyAlgorithm,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId
+ }: TCreateCaDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Create,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ const dn = createDistinguishedName({
+ commonName,
+ organization,
+ ou,
+ country,
+ province,
+ locality
+ });
+
+ const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
+ const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
+
+ const newCa = await certificateAuthorityDAL.transaction(async (tx) => {
+ const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
+
+ // if undefined, set [notAfterDate] to 10 years from now
+ const notAfterDate = notAfter
+ ? new Date(notAfter)
+ : new Date(new Date().setFullYear(new Date().getFullYear() + 10));
+
+ const serialNumber = crypto.randomBytes(32).toString("hex");
+
+ const ca = await certificateAuthorityDAL.create(
+ {
+ projectId: project.id,
+ type,
+ organization,
+ ou,
+ country,
+ province,
+ locality,
+ friendlyName: friendlyName || dn,
+ commonName,
+ status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE,
+ dn,
+ keyAlgorithm,
+ ...(type === CaType.ROOT && {
+ maxPathLength,
+ notBefore: notBeforeDate,
+ notAfter: notAfterDate,
+ serialNumber
+ })
+ },
+ tx
+ );
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: project.id,
+ projectDAL,
+ kmsService
+ });
+
+ if (type === CaType.ROOT) {
+ // note: create self-signed cert only applicable for root CA
+ const cert = await x509.X509CertificateGenerator.createSelfSigned({
+ name: dn,
+ serialNumber,
+ notBefore: notBeforeDate,
+ notAfter: notAfterDate,
+ signingAlgorithm: alg,
+ keys,
+ extensions: [
+ new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
+ new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
+ // eslint-disable-next-line no-bitwise
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
+ await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
+ ]
+ });
+
+ const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.from(new Uint8Array(cert.rawData))
+ });
+
+ const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.alloc(0)
+ });
+
+ await certificateAuthorityCertDAL.create(
+ {
+ caId: ca.id,
+ encryptedCertificate,
+ encryptedCertificateChain
+ },
+ tx
+ );
+ }
+
+ // create empty CRL
+ const crl = await x509.X509CrlGenerator.create({
+ issuer: ca.dn,
+ thisUpdate: new Date(),
+ nextUpdate: new Date("2025/12/12"), // TODO: change
+ entries: [],
+ signingAlgorithm: alg,
+ signingKey: keys.privateKey
+ });
+
+ const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.from(new Uint8Array(crl.rawData))
+ });
+
+ await certificateAuthorityCrlDAL.create(
+ {
+ caId: ca.id,
+ encryptedCrl
+ },
+ tx
+ );
+
+ // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
+ const skObj = KeyObject.from(keys.privateKey);
+
+ const { cipherTextBlob: encryptedPrivateKey } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: skObj.export({
+ type: "pkcs8",
+ format: "der"
+ })
+ });
+
+ await certificateAuthoritySecretDAL.create(
+ {
+ caId: ca.id,
+ encryptedPrivateKey
+ },
+ tx
+ );
+
+ return ca;
+ });
+
+ return newCa;
+ };
+
+ /**
+ * Return CA with id [caId]
+ */
+ const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ return ca;
+ };
+
+ /**
+ * Update CA with id [caId].
+ * Note: Used to enable/disable CA
+ */
+ const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Edit,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ const updatedCa = await certificateAuthorityDAL.updateById(caId, { status });
+
+ return updatedCa;
+ };
+
+ /**
+ * Delete CA with id [caId]
+ */
+ const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Delete,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ const deletedCa = await certificateAuthorityDAL.deleteById(caId);
+
+ return deletedCa;
+ };
+
+ /**
+ * Return certificate signing request (CSR) made with CA with id [caId]
+ */
+ const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Create,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
+
+ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
+ if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
+
+ const { caPrivateKey, caPublicKey } = await getCaCredentials({
+ caId,
+ certificateAuthorityDAL,
+ certificateAuthoritySecretDAL,
+ projectDAL,
+ kmsService
+ });
+
+ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
+
+ const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
+ name: ca.dn,
+ keys: {
+ privateKey: caPrivateKey,
+ publicKey: caPublicKey
+ },
+ signingAlgorithm: alg,
+ extensions: [
+ // eslint-disable-next-line no-bitwise
+ new x509.KeyUsagesExtension(
+ x509.KeyUsageFlags.keyCertSign |
+ x509.KeyUsageFlags.cRLSign |
+ x509.KeyUsageFlags.digitalSignature |
+ x509.KeyUsageFlags.keyEncipherment
+ )
+ ],
+ attributes: [new x509.ChallengePasswordAttribute("password")]
+ });
+
+ return {
+ csr: csrObj.toString("pem"),
+ ca
+ };
+ };
+
+ /**
+ * Return certificate and certificate chain for CA
+ */
+ const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ const { caCert, caCertChain, serialNumber } = await getCaCertChain({
+ caId,
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ projectDAL,
+ kmsService
+ });
+
+ return {
+ certificate: caCert,
+ certificateChain: caCertChain,
+ serialNumber,
+ ca
+ };
+ };
+
+ /**
+ * Issue certificate to be imported back in for intermediate CA
+ */
+ const signIntermediate = async ({
+ caId,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId,
+ csr,
+ notBefore,
+ notAfter,
+ maxPathLength
+ }: TSignIntermediateDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Create,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
+
+ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
+ const decryptedCaCert = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caCert.encryptedCertificate
+ });
+
+ const caCertObj = new x509.X509Certificate(decryptedCaCert);
+ const csrObj = new x509.Pkcs10CertificateRequest(csr);
+
+ // check path length constraint
+ const caPathLength = caCertObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
+ if (caPathLength !== undefined) {
+ if (caPathLength === 0)
+ throw new BadRequestError({
+ message: "Failed to issue intermediate certificate due to CA path length constraint"
+ });
+ if (maxPathLength >= caPathLength || (maxPathLength === -1 && caPathLength !== -1))
+ throw new BadRequestError({
+ message: "The requested path length constraint exceeds the CA's allowed path length"
+ });
+ }
+
+ const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
+ const notAfterDate = new Date(notAfter);
+
+ const caCertNotBeforeDate = new Date(caCertObj.notBefore);
+ const caCertNotAfterDate = new Date(caCertObj.notAfter);
+
+ // check not before constraint
+ if (notBeforeDate < caCertNotBeforeDate) {
+ throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
+ }
+
+ if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
+
+ // check not after constraint
+ if (notAfterDate > caCertNotAfterDate) {
+ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
+ }
+
+ const { caPrivateKey } = await getCaCredentials({
+ caId: ca.id,
+ certificateAuthorityDAL,
+ certificateAuthoritySecretDAL,
+ projectDAL,
+ kmsService
+ });
+
+ const serialNumber = crypto.randomBytes(32).toString("hex");
+ const intermediateCert = await x509.X509CertificateGenerator.create({
+ serialNumber,
+ subject: csrObj.subject,
+ issuer: caCertObj.subject,
+ notBefore: notBeforeDate,
+ notAfter: notAfterDate,
+ signingKey: caPrivateKey,
+ publicKey: csrObj.publicKey,
+ signingAlgorithm: alg,
+ extensions: [
+ new x509.KeyUsagesExtension(
+ x509.KeyUsageFlags.keyCertSign |
+ x509.KeyUsageFlags.cRLSign |
+ x509.KeyUsageFlags.digitalSignature |
+ x509.KeyUsageFlags.keyEncipherment,
+ true
+ ),
+ new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
+ await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
+ await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
+ ]
+ });
+
+ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
+ caId,
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ projectDAL,
+ kmsService
+ });
+
+ return {
+ certificate: intermediateCert.toString("pem"),
+ issuingCaCertificate,
+ certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
+ serialNumber: intermediateCert.serialNumber,
+ ca
+ };
+ };
+
+ /**
+ * Import certificate for (un-installed) CA with id [caId].
+ * Note: Can be used to import an external certificate and certificate chain
+ * to be installed into the CA.
+ */
+ const importCertToCa = async ({
+ caId,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId,
+ certificate,
+ certificateChain
+ }: TImportCertToCaDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Create,
+ ProjectPermissionSub.CertificateAuthorities
+ );
+
+ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
+ if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
+
+ const certObj = new x509.X509Certificate(certificate);
+ const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
+
+ // validate imported certificate and certificate chain
+ const certificates = certificateChain
+ .match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
+ ?.map((cert) => new x509.X509Certificate(cert));
+
+ if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
+
+ const chain = new x509.X509ChainBuilder({
+ certificates
+ });
+
+ const chainItems = await chain.build(certObj);
+
+ // chain.build() implicitly verifies the chain
+ if (chainItems.length !== certificates.length + 1)
+ throw new BadRequestError({ message: "Invalid certificate chain" });
+
+ const parentCertObj = chainItems[1];
+ const parentCertSubject = parentCertObj.subject;
+
+ const parentCa = await certificateAuthorityDAL.findOne({
+ projectId: ca.projectId,
+ dn: parentCertSubject
+ });
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.from(new Uint8Array(certObj.rawData))
+ });
+
+ const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.from(certificateChain)
+ });
+
+ await certificateAuthorityCertDAL.transaction(async (tx) => {
+ await certificateAuthorityCertDAL.create(
+ {
+ caId: ca.id,
+ encryptedCertificate,
+ encryptedCertificateChain
+ },
+ tx
+ );
+
+ await certificateAuthorityDAL.updateById(
+ ca.id,
+ {
+ status: CaStatus.ACTIVE,
+ maxPathLength: maxPathLength === undefined ? -1 : maxPathLength,
+ notBefore: new Date(certObj.notBefore),
+ notAfter: new Date(certObj.notAfter),
+ serialNumber: certObj.serialNumber,
+ parentCaId: parentCa?.id
+ },
+ tx
+ );
+ });
+
+ return { ca };
+ };
+
+ /**
+ * Return new leaf certificate issued by CA with id [caId]
+ */
+ const issueCertFromCa = async ({
+ caId,
+ friendlyName,
+ commonName,
+ ttl,
+ notBefore,
+ notAfter,
+ actorId,
+ actorAuthMethod,
+ actor,
+ actorOrgId
+ }: TIssueCertFromCaDTO) => {
+ const ca = await certificateAuthorityDAL.findById(caId);
+ if (!ca) throw new BadRequestError({ message: "CA not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ ca.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
+
+ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
+
+ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
+ if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
+
+ const keyId = await getProjectKmsCertificateKeyId({
+ projectId: ca.projectId,
+ projectDAL,
+ kmsService
+ });
+
+ const decryptedCaCert = await kmsService.decrypt({
+ kmsId: keyId,
+ cipherTextBlob: caCert.encryptedCertificate
+ });
+
+ const caCertObj = new x509.X509Certificate(decryptedCaCert);
+
+ const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
+
+ let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
+ if (notAfter) {
+ notAfterDate = new Date(notAfter);
+ } else if (ttl) {
+ notAfterDate = new Date(new Date().getTime() + ms(ttl));
+ }
+
+ const caCertNotBeforeDate = new Date(caCertObj.notBefore);
+ const caCertNotAfterDate = new Date(caCertObj.notAfter);
+
+ // check not before constraint
+ if (notBeforeDate < caCertNotBeforeDate) {
+ throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
+ }
+
+ if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
+
+ // check not after constraint
+ if (notAfterDate > caCertNotAfterDate) {
+ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
+ }
+
+ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
+ const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
+
+ const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
+ name: `CN=${commonName}`,
+ keys: leafKeys,
+ signingAlgorithm: alg,
+ extensions: [
+ // eslint-disable-next-line no-bitwise
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
+ ],
+ attributes: [new x509.ChallengePasswordAttribute("password")]
+ });
+
+ const { caPrivateKey } = await getCaCredentials({
+ caId: ca.id,
+ certificateAuthorityDAL,
+ certificateAuthoritySecretDAL,
+ projectDAL,
+ kmsService
+ });
+
+ const serialNumber = crypto.randomBytes(32).toString("hex");
+ const leafCert = await x509.X509CertificateGenerator.create({
+ serialNumber,
+ subject: csrObj.subject,
+ issuer: caCertObj.subject,
+ notBefore: notBeforeDate,
+ notAfter: notAfterDate,
+ signingKey: caPrivateKey,
+ publicKey: csrObj.publicKey,
+ signingAlgorithm: alg,
+ extensions: [
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
+ new x509.BasicConstraintsExtension(false),
+ await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
+ await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
+ ]
+ });
+
+ const skLeafObj = KeyObject.from(leafKeys.privateKey);
+ const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
+
+ const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({
+ kmsId: keyId,
+ plainText: Buffer.from(new Uint8Array(leafCert.rawData))
+ });
+
+ await certificateDAL.transaction(async (tx) => {
+ const cert = await certificateDAL.create(
+ {
+ caId: ca.id,
+ status: CertStatus.ACTIVE,
+ friendlyName: friendlyName || commonName,
+ commonName,
+ serialNumber,
+ notBefore: notBeforeDate,
+ notAfter: notAfterDate
+ },
+ tx
+ );
+
+ await certificateBodyDAL.create(
+ {
+ certId: cert.id,
+ encryptedCertificate
+ },
+ tx
+ );
+
+ return cert;
+ });
+
+ const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
+ caId: ca.id,
+ certificateAuthorityDAL,
+ certificateAuthorityCertDAL,
+ projectDAL,
+ kmsService
+ });
+
+ return {
+ certificate: leafCert.toString("pem"),
+ certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
+ issuingCaCertificate,
+ privateKey: skLeaf,
+ serialNumber,
+ ca
+ };
+ };
+
+ return {
+ createCa,
+ getCaById,
+ updateCaById,
+ deleteCaById,
+ getCaCsr,
+ getCaCert,
+ signIntermediate,
+ importCertToCa,
+ issueCertFromCa
+ };
+};
diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts
new file mode 100644
index 000000000..3ba7624c0
--- /dev/null
+++ b/backend/src/services/certificate-authority/certificate-authority-types.ts
@@ -0,0 +1,121 @@
+import { TProjectPermission } from "@app/lib/types";
+import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
+import { TKmsServiceFactory } from "@app/services/kms/kms-service";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
+
+import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
+import { CertKeyAlgorithm } from "../certificate/certificate-types";
+import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
+import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
+import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
+
+export enum CaType {
+ ROOT = "root",
+ INTERMEDIATE = "intermediate"
+}
+
+export enum CaStatus {
+ ACTIVE = "active",
+ DISABLED = "disabled",
+ PENDING_CERTIFICATE = "pending-certificate"
+}
+
+export type TCreateCaDTO = {
+ projectSlug: string;
+ type: CaType;
+ friendlyName?: string;
+ commonName: string;
+ organization: string;
+ ou: string;
+ country: string;
+ province: string;
+ locality: string;
+ notBefore?: string;
+ notAfter?: string;
+ maxPathLength: number;
+ keyAlgorithm: CertKeyAlgorithm;
+} & Omit;
+
+export type TGetCaDTO = {
+ caId: string;
+} & Omit;
+
+export type TUpdateCaDTO = {
+ caId: string;
+ status?: CaStatus;
+} & Omit;
+
+export type TDeleteCaDTO = {
+ caId: string;
+} & Omit;
+
+export type TGetCaCsrDTO = {
+ caId: string;
+} & Omit;
+
+export type TGetCaCertDTO = {
+ caId: string;
+} & Omit;
+
+export type TSignIntermediateDTO = {
+ caId: string;
+ csr: string;
+ notBefore?: string;
+ notAfter: string;
+ maxPathLength: number;
+} & Omit;
+
+export type TImportCertToCaDTO = {
+ caId: string;
+ certificate: string;
+ certificateChain: string;
+} & Omit;
+
+export type TIssueCertFromCaDTO = {
+ caId: string;
+ friendlyName?: string;
+ commonName: string;
+ ttl: string;
+ notBefore?: string;
+ notAfter?: string;
+} & Omit;
+
+export type TDNParts = {
+ commonName?: string;
+ organization?: string;
+ ou?: string;
+ country?: string;
+ province?: string;
+ locality?: string;
+};
+
+export type TGetCaCredentialsDTO = {
+ caId: string;
+ certificateAuthorityDAL: Pick;
+ certificateAuthoritySecretDAL: Pick;
+ projectDAL: Pick;
+ kmsService: Pick;
+};
+
+export type TGetCaCertChainDTO = {
+ caId: string;
+ certificateAuthorityDAL: Pick;
+ certificateAuthorityCertDAL: Pick;
+ projectDAL: Pick;
+ kmsService: Pick;
+};
+
+export type TRebuildCaCrlDTO = {
+ caId: string;
+ certificateAuthorityDAL: Pick;
+ certificateAuthorityCrlDAL: Pick;
+ certificateAuthoritySecretDAL: Pick