diff --git a/.env.example b/.env.example
index 8463fea92..bdb3e536d 100644
--- a/.env.example
+++ b/.env.example
@@ -3,9 +3,6 @@
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
-# Required
-DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
-
# JWT
# Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
@@ -16,6 +13,9 @@ POSTGRES_PASSWORD=infisical
POSTGRES_USER=infisical
POSTGRES_DB=infisical
+# Required
+DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
+
# Redis
REDIS_URL=redis://redis:6379
diff --git a/.github/workflows/build-docker-image-to-prod.yml b/.github/workflows/build-docker-image-to-prod.yml
index d1ae80dad..3818fa1f8 100644
--- a/.github/workflows/build-docker-image-to-prod.yml
+++ b/.github/workflows/build-docker-image-to-prod.yml
@@ -41,6 +41,7 @@ jobs:
load: true
context: backend
tags: infisical/infisical:test
+ platforms: linux/amd64,linux/arm64
- name: โป Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@@ -92,6 +93,7 @@ jobs:
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
+ platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
diff --git a/.github/workflows/build-staging-and-deploy-aws.yml b/.github/workflows/build-staging-and-deploy-aws.yml
new file mode 100644
index 000000000..78a193d30
--- /dev/null
+++ b/.github/workflows/build-staging-and-deploy-aws.yml
@@ -0,0 +1,140 @@
+name: Deployment pipeline
+on: [workflow_dispatch]
+
+permissions:
+ id-token: write
+ contents: read
+
+jobs:
+ infisical-image:
+ name: Build backend image
+ runs-on: ubuntu-latest
+ steps:
+ - name: โ๏ธ Checkout source
+ uses: actions/checkout@v3
+ - name: ๐ฆ Install dependencies to test all dependencies
+ run: npm ci --only-production
+ working-directory: backend
+ - name: Save commit hashes for tag
+ id: commit
+ uses: pr-mpt/actions-commit-hash@v2
+ - name: ๐ง Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+ - name: ๐ Login to Docker Hub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Set up Depot CLI
+ uses: depot/setup-action@v1
+ - name: ๐๏ธ Build backend and push to docker hub
+ uses: depot/build-push-action@v1
+ with:
+ project: 64mmf0n610
+ token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
+ push: true
+ context: .
+ file: Dockerfile.standalone-infisical
+ tags: |
+ infisical/staging_infisical:${{ steps.commit.outputs.short }}
+ infisical/staging_infisical:latest
+ platforms: linux/amd64,linux/arm64
+ build-args: |
+ POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
+ INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }}
+
+ gamma-deployment:
+ name: Deploy to gamma
+ runs-on: ubuntu-latest
+ needs: [infisical-image]
+ environment:
+ name: Gamma
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Setup Node.js environment
+ uses: actions/setup-node@v2
+ with:
+ node-version: "20"
+ - name: Change directory to backend and install dependencies
+ env:
+ DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
+ run: |
+ cd backend
+ npm install
+ npm run migration:latest
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ audience: sts.amazonaws.com
+ aws-region: us-east-1
+ role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
+ - name: Save commit hashes for tag
+ id: commit
+ uses: pr-mpt/actions-commit-hash@v2
+ - name: Download task definition
+ run: |
+ aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
+ - name: Render Amazon ECS task definition
+ id: render-web-container
+ uses: aws-actions/amazon-ecs-render-task-definition@v1
+ with:
+ task-definition: task-definition.json
+ container-name: infisical-prod-platform
+ image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
+ environment-variables: "LOG_LEVEL=info"
+ - name: Deploy to Amazon ECS service
+ uses: aws-actions/amazon-ecs-deploy-task-definition@v1
+ with:
+ task-definition: ${{ steps.render-web-container.outputs.task-definition }}
+ service: infisical-prod-platform
+ cluster: infisical-prod-platform
+ wait-for-service-stability: true
+
+ production-postgres-deployment:
+ name: Deploy to production
+ runs-on: ubuntu-latest
+ needs: [gamma-deployment]
+ environment:
+ name: Production
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Setup Node.js environment
+ uses: actions/setup-node@v2
+ with:
+ node-version: "20"
+ - name: Change directory to backend and install dependencies
+ env:
+ DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
+ run: |
+ cd backend
+ npm install
+ npm run migration:latest
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ audience: sts.amazonaws.com
+ aws-region: us-east-1
+ role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment
+ - name: Save commit hashes for tag
+ id: commit
+ uses: pr-mpt/actions-commit-hash@v2
+ - name: Download task definition
+ run: |
+ aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
+ - name: Render Amazon ECS task definition
+ id: render-web-container
+ uses: aws-actions/amazon-ecs-render-task-definition@v1
+ with:
+ task-definition: task-definition.json
+ container-name: infisical-prod-platform
+ image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
+ environment-variables: "LOG_LEVEL=info"
+ - name: Deploy to Amazon ECS service
+ uses: aws-actions/amazon-ecs-deploy-task-definition@v1
+ with:
+ task-definition: ${{ steps.render-web-container.outputs.task-definition }}
+ service: infisical-prod-platform
+ cluster: infisical-prod-platform
+ wait-for-service-stability: true
diff --git a/.github/workflows/build-staging-and-deploy.yml b/.github/workflows/build-staging-and-deploy.yml
deleted file mode 100644
index 31ffb8729..000000000
--- a/.github/workflows/build-staging-and-deploy.yml
+++ /dev/null
@@ -1,120 +0,0 @@
-name: Build, Publish and Deploy to Gamma
-on: [workflow_dispatch]
-
-jobs:
- infisical-image:
- name: Build backend image
- runs-on: ubuntu-latest
- steps:
- - name: โ๏ธ Checkout source
- uses: actions/checkout@v3
- - name: ๐ฆ Install dependencies to test all dependencies
- run: npm ci --only-production
- working-directory: backend
- # - name: ๐งช Run tests
- # run: npm run test:ci
- # working-directory: backend
- - name: Save commit hashes for tag
- id: commit
- uses: pr-mpt/actions-commit-hash@v2
- - name: ๐ง Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
- - name: ๐ Login to Docker Hub
- uses: docker/login-action@v2
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Set up Depot CLI
- uses: depot/setup-action@v1
- - name: ๐ฆ Build backend and export to Docker
- uses: depot/build-push-action@v1
- with:
- project: 64mmf0n610
- token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
- load: true
- context: .
- file: Dockerfile.standalone-infisical
- tags: infisical/infisical:test
- # - name: โป Spawn backend container and dependencies
- # run: |
- # docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- # - name: ๐งช Test backend image
- # run: |
- # ./.github/resources/healthcheck.sh infisical-backend-test
- # - name: โป Shut down backend container and dependencies
- # run: |
- # docker compose -f .github/resources/docker-compose.be-test.yml down
- - name: ๐๏ธ Build backend and push
- uses: depot/build-push-action@v1
- with:
- project: 64mmf0n610
- token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
- push: true
- context: .
- file: Dockerfile.standalone-infisical
- tags: |
- infisical/staging_infisical:${{ steps.commit.outputs.short }}
- infisical/staging_infisical:latest
- platforms: linux/amd64,linux/arm64
- build-args: |
- POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- postgres-migration:
- name: Run latest migration files
- runs-on: ubuntu-latest
- needs: [infisical-image]
- steps:
- - name: Checkout code
- uses: actions/checkout@v2
- - name: Setup Node.js environment
- uses: actions/setup-node@v2
- with:
- node-version: "20"
- - name: Change directory to backend and install dependencies
- env:
- DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
- run: |
- cd backend
- npm install
- npm run migration:latest
- # - name: Run postgres DB migration files
- # env:
- # DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
- # run: npm run migration:latest
- gamma-deployment:
- name: Deploy to gamma
- runs-on: ubuntu-latest
- needs: [postgres-migration]
- steps:
- - name: โ๏ธ Checkout source
- uses: actions/checkout@v3
- - name: Install Helm
- uses: azure/setup-helm@v3
- with:
- version: v3.10.0
- - name: Install infisical helm chart
- run: |
- helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
- helm repo update
- - name: Install kubectl
- uses: azure/setup-kubectl@v3
- - name: Install doctl
- uses: digitalocean/action-doctl@v2
- with:
- token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- - name: Save DigitalOcean kubeconfig with short-lived credentials
- run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 infisical-gamma-postgres
- - name: switch to gamma namespace
- run: kubectl config set-context --current --namespace=gamma
- - name: test kubectl
- run: kubectl get ingress
- - name: Download helm values to file and upgrade gamma deploy
- run: |
- wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
- helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
- if [[ $(helm status infisical) == *"FAILED"* ]]; then
- echo "Helm upgrade failed"
- exit 1
- else
- echo "Helm upgrade was successful"
- fi
diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml
index 7fba6c321..2086601a8 100644
--- a/.github/workflows/check-api-for-breaking-changes.yml
+++ b/.github/workflows/check-api-for-breaking-changes.yml
@@ -5,6 +5,7 @@ on:
types: [opened, synchronize]
paths:
- "backend/src/server/routes/**"
+ - "backend/src/ee/routes/**"
jobs:
check-be-api-changes:
diff --git a/.gitignore b/.gitignore
index af3d457e0..4a12c15f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,8 @@ yarn-error.log*
# Infisical init
.infisical.json
+.infisicalignore
+
# Editor specific
.vscode/*
diff --git a/.infisicalignore b/.infisicalignore
index b8fafe6db..348f9e327 100644
--- a/.infisicalignore
+++ b/.infisicalignore
@@ -1 +1,5 @@
.github/resources/docker-compose.be-test.yml:generic-api-key:16
+frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
+frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
+frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
+frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
\ No newline at end of file
diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical
index d4596115e..737067534 100644
--- a/Dockerfile.standalone-infisical
+++ b/Dockerfile.standalone-infisical
@@ -1,6 +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
FROM node:20-alpine AS base
@@ -35,6 +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
# Build
RUN npm run build
@@ -100,6 +103,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
WORKDIR /
@@ -118,9 +124,6 @@ WORKDIR /backend
ENV TELEMETRY_ENABLED true
-HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
- CMD node healthcheck.js
-
EXPOSE 8080
EXPOSE 443
diff --git a/README.md b/README.md
index 3c2fd5387..74e86ced3 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,8 @@
Infisical Cloud |
Self-Hosting |
Docs |
- Website
+ Website |
+ Hiring (Remote/SF)
diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js
index 9c558919b..b23cf05ae 100644
--- a/backend/.eslintrc.js
+++ b/backend/.eslintrc.js
@@ -23,16 +23,17 @@ module.exports = {
root: true,
overrides: [
{
- files: ["./e2e-test/**/*"],
+ files: ["./e2e-test/**/*", "./src/db/migrations/**/*"],
rules: {
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-return": "off",
- "@typescript-eslint/no-unsafe-call": "off",
+ "@typescript-eslint/no-unsafe-call": "off"
}
}
],
+
rules: {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
diff --git a/backend/e2e-test/routes/v1/secret-import.spec.ts b/backend/e2e-test/routes/v1/secret-import.spec.ts
index ba37b5f42..c184e44e5 100644
--- a/backend/e2e-test/routes/v1/secret-import.spec.ts
+++ b/backend/e2e-test/routes/v1/secret-import.spec.ts
@@ -46,7 +46,7 @@ const deleteSecretImport = async (id: string) => {
describe("Secret Import Router", async () => {
test.each([
- { importEnv: "dev", importPath: "/" }, // one in root
+ { importEnv: "prod", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
// check for default environments
@@ -66,7 +66,7 @@ describe("Secret Import Router", async () => {
});
test("Get secret imports", async () => {
- const createdImport1 = await createSecretImport("/", "dev");
+ const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const res = await testServer.inject({
method: "GET",
@@ -103,10 +103,10 @@ describe("Secret Import Router", async () => {
});
test("Update secret import position", async () => {
- const devImportDetails = { path: "/", envSlug: "dev" };
+ const prodImportDetails = { path: "/", envSlug: "prod" };
const stagingImportDetails = { path: "/", envSlug: "staging" };
- const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
+ const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
const updateImportRes = await testServer.inject({
@@ -136,7 +136,7 @@ describe("Secret Import Router", async () => {
position: 2,
importEnv: expect.objectContaining({
name: expect.any(String),
- slug: expect.stringMatching(devImportDetails.envSlug),
+ slug: expect.stringMatching(prodImportDetails.envSlug),
id: expect.any(String)
})
})
@@ -166,7 +166,7 @@ describe("Secret Import Router", async () => {
});
test("Delete secret import position", async () => {
- const createdImport1 = await createSecretImport("/", "dev");
+ const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const deletedImport = await deleteSecretImport(createdImport1.id);
// check for default environments
diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts
index c1c750225..09ab05443 100644
--- a/backend/e2e-test/vitest-environment-knex.ts
+++ b/backend/e2e-test/vitest-environment-knex.ts
@@ -10,7 +10,7 @@ import { seedData1 } from "@app/db/seed-data";
import { initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app";
-import { AuthTokenType } from "@app/services/auth/auth-type";
+import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { mockQueue } from "./mocks/queue";
import { mockSmtpServer } from "./mocks/smtp";
@@ -52,6 +52,8 @@ export default {
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: seedData1.id,
tokenVersionId: seedData1.token.id,
+ authMethod: AuthMethod.EMAIL,
+ organizationId: seedData1.organization.id,
accessVersion: 1
},
cfg.AUTH_SECRET,
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 97eaf4ff8..1ef6c19e2 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -47,10 +47,11 @@
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
- "mysql2": "^3.9.1",
+ "mysql2": "^3.9.4",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",
+ "oracledb": "^6.4.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
@@ -1708,6 +1709,22 @@
"node": ">=12"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
+ "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@@ -3162,9 +3179,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.8.0.tgz",
- "integrity": "sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz",
+ "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==",
"cpu": [
"arm"
],
@@ -3175,9 +3192,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.8.0.tgz",
- "integrity": "sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz",
+ "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==",
"cpu": [
"arm64"
],
@@ -3188,9 +3205,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.8.0.tgz",
- "integrity": "sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
+ "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
"cpu": [
"arm64"
],
@@ -3201,9 +3218,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.8.0.tgz",
- "integrity": "sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz",
+ "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==",
"cpu": [
"x64"
],
@@ -3214,9 +3231,22 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.8.0.tgz",
- "integrity": "sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz",
+ "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz",
+ "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==",
"cpu": [
"arm"
],
@@ -3227,9 +3257,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.8.0.tgz",
- "integrity": "sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
+ "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
"cpu": [
"arm64"
],
@@ -3240,9 +3270,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.8.0.tgz",
- "integrity": "sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz",
+ "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==",
"cpu": [
"arm64"
],
@@ -3252,10 +3282,23 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz",
+ "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.8.0.tgz",
- "integrity": "sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz",
+ "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==",
"cpu": [
"riscv64"
],
@@ -3265,10 +3308,23 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz",
+ "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.8.0.tgz",
- "integrity": "sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
+ "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
"cpu": [
"x64"
],
@@ -3279,9 +3335,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.8.0.tgz",
- "integrity": "sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz",
+ "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==",
"cpu": [
"x64"
],
@@ -3292,9 +3348,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.8.0.tgz",
- "integrity": "sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz",
+ "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==",
"cpu": [
"arm64"
],
@@ -3305,9 +3361,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.8.0.tgz",
- "integrity": "sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz",
+ "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==",
"cpu": [
"ia32"
],
@@ -3318,9 +3374,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.8.0.tgz",
- "integrity": "sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz",
+ "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==",
"cpu": [
"x64"
],
@@ -5916,12 +5972,12 @@
}
},
"node_modules/body-parser": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
- "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+ "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
- "content-type": "~1.0.4",
+ "content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
@@ -5929,7 +5985,7 @@
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
- "raw-body": "2.5.1",
+ "raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
@@ -7379,16 +7435,16 @@
}
},
"node_modules/express": {
- "version": "4.18.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
- "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
+ "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
- "body-parser": "1.20.1",
+ "body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.5.0",
+ "cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -7419,6 +7475,14 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express/node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/express/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -7749,9 +7813,9 @@
"dev": true
},
"node_modules/follow-redirects": {
- "version": "1.15.4",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
- "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -9759,9 +9823,9 @@
}
},
"node_modules/mysql2": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
- "integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
+ "version": "3.9.4",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
+ "integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -10231,6 +10295,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/oracledb": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.4.0.tgz",
+ "integrity": "sha512-TJI08qzQlf/l7T49VojP9BoQpjEr14NXZmpSzzcLrbNs7qSl0QA/Mc9gGiEdkg5WmwH0wqUjtMC7jlf1WamlYA==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=14.6"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -10818,9 +10891,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.32",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
- "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"dev": true,
"funding": [
{
@@ -10839,7 +10912,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -11234,9 +11307,9 @@
}
},
"node_modules/raw-body": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
- "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
@@ -11511,10 +11584,13 @@
}
},
"node_modules/rollup": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.8.0.tgz",
- "integrity": "sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA==",
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz",
+ "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==",
"dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.5"
+ },
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -11523,19 +11599,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.8.0",
- "@rollup/rollup-android-arm64": "4.8.0",
- "@rollup/rollup-darwin-arm64": "4.8.0",
- "@rollup/rollup-darwin-x64": "4.8.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.8.0",
- "@rollup/rollup-linux-arm64-gnu": "4.8.0",
- "@rollup/rollup-linux-arm64-musl": "4.8.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.8.0",
- "@rollup/rollup-linux-x64-gnu": "4.8.0",
- "@rollup/rollup-linux-x64-musl": "4.8.0",
- "@rollup/rollup-win32-arm64-msvc": "4.8.0",
- "@rollup/rollup-win32-ia32-msvc": "4.8.0",
- "@rollup/rollup-win32-x64-msvc": "4.8.0",
+ "@rollup/rollup-android-arm-eabi": "4.14.3",
+ "@rollup/rollup-android-arm64": "4.14.3",
+ "@rollup/rollup-darwin-arm64": "4.14.3",
+ "@rollup/rollup-darwin-x64": "4.14.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.14.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.14.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.14.3",
+ "@rollup/rollup-linux-arm64-musl": "4.14.3",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.14.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.14.3",
+ "@rollup/rollup-linux-x64-gnu": "4.14.3",
+ "@rollup/rollup-linux-x64-musl": "4.14.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.14.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.14.3",
+ "@rollup/rollup-win32-x64-msvc": "4.14.3",
"fsevents": "~2.3.2"
}
},
@@ -11887,9 +11966,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -12249,9 +12328,9 @@
}
},
"node_modules/tar": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
- "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@@ -13447,14 +13526,14 @@
}
},
"node_modules/vite": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
- "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
+ "version": "5.2.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz",
+ "integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==",
"dev": true,
"dependencies": {
- "esbuild": "^0.19.3",
- "postcss": "^8.4.32",
- "rollup": "^4.2.0"
+ "esbuild": "^0.20.1",
+ "postcss": "^8.4.38",
+ "rollup": "^4.13.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -13589,9 +13668,9 @@
"dev": true
},
"node_modules/vite/node_modules/@esbuild/android-arm": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.9.tgz",
- "integrity": "sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
+ "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
@@ -13605,9 +13684,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/android-arm64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.9.tgz",
- "integrity": "sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
+ "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
@@ -13621,9 +13700,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/android-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.9.tgz",
- "integrity": "sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
+ "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
@@ -13637,9 +13716,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.9.tgz",
- "integrity": "sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
+ "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
@@ -13653,9 +13732,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/darwin-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.9.tgz",
- "integrity": "sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
+ "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
@@ -13669,9 +13748,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.9.tgz",
- "integrity": "sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
+ "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
@@ -13685,9 +13764,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/freebsd-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.9.tgz",
- "integrity": "sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
+ "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
@@ -13701,9 +13780,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-arm": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.9.tgz",
- "integrity": "sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
+ "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
@@ -13717,9 +13796,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-arm64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.9.tgz",
- "integrity": "sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
+ "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
@@ -13733,9 +13812,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-ia32": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.9.tgz",
- "integrity": "sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
+ "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
@@ -13749,9 +13828,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-loong64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.9.tgz",
- "integrity": "sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
+ "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
@@ -13765,9 +13844,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-mips64el": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.9.tgz",
- "integrity": "sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
+ "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
@@ -13781,9 +13860,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-ppc64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.9.tgz",
- "integrity": "sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
+ "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
@@ -13797,9 +13876,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-riscv64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.9.tgz",
- "integrity": "sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
+ "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
@@ -13813,9 +13892,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-s390x": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.9.tgz",
- "integrity": "sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
+ "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
@@ -13829,9 +13908,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.9.tgz",
- "integrity": "sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
+ "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
@@ -13845,9 +13924,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/netbsd-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.9.tgz",
- "integrity": "sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
@@ -13861,9 +13940,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/openbsd-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.9.tgz",
- "integrity": "sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
@@ -13877,9 +13956,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/sunos-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.9.tgz",
- "integrity": "sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
+ "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
@@ -13893,9 +13972,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/win32-arm64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.9.tgz",
- "integrity": "sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
+ "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
@@ -13909,9 +13988,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/win32-ia32": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.9.tgz",
- "integrity": "sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
+ "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
@@ -13925,9 +14004,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/win32-x64": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.9.tgz",
- "integrity": "sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
+ "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
@@ -13941,9 +14020,9 @@
}
},
"node_modules/vite/node_modules/esbuild": {
- "version": "0.19.9",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.9.tgz",
- "integrity": "sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
+ "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"bin": {
@@ -13953,28 +14032,29 @@
"node": ">=12"
},
"optionalDependencies": {
- "@esbuild/android-arm": "0.19.9",
- "@esbuild/android-arm64": "0.19.9",
- "@esbuild/android-x64": "0.19.9",
- "@esbuild/darwin-arm64": "0.19.9",
- "@esbuild/darwin-x64": "0.19.9",
- "@esbuild/freebsd-arm64": "0.19.9",
- "@esbuild/freebsd-x64": "0.19.9",
- "@esbuild/linux-arm": "0.19.9",
- "@esbuild/linux-arm64": "0.19.9",
- "@esbuild/linux-ia32": "0.19.9",
- "@esbuild/linux-loong64": "0.19.9",
- "@esbuild/linux-mips64el": "0.19.9",
- "@esbuild/linux-ppc64": "0.19.9",
- "@esbuild/linux-riscv64": "0.19.9",
- "@esbuild/linux-s390x": "0.19.9",
- "@esbuild/linux-x64": "0.19.9",
- "@esbuild/netbsd-x64": "0.19.9",
- "@esbuild/openbsd-x64": "0.19.9",
- "@esbuild/sunos-x64": "0.19.9",
- "@esbuild/win32-arm64": "0.19.9",
- "@esbuild/win32-ia32": "0.19.9",
- "@esbuild/win32-x64": "0.19.9"
+ "@esbuild/aix-ppc64": "0.20.2",
+ "@esbuild/android-arm": "0.20.2",
+ "@esbuild/android-arm64": "0.20.2",
+ "@esbuild/android-x64": "0.20.2",
+ "@esbuild/darwin-arm64": "0.20.2",
+ "@esbuild/darwin-x64": "0.20.2",
+ "@esbuild/freebsd-arm64": "0.20.2",
+ "@esbuild/freebsd-x64": "0.20.2",
+ "@esbuild/linux-arm": "0.20.2",
+ "@esbuild/linux-arm64": "0.20.2",
+ "@esbuild/linux-ia32": "0.20.2",
+ "@esbuild/linux-loong64": "0.20.2",
+ "@esbuild/linux-mips64el": "0.20.2",
+ "@esbuild/linux-ppc64": "0.20.2",
+ "@esbuild/linux-riscv64": "0.20.2",
+ "@esbuild/linux-s390x": "0.20.2",
+ "@esbuild/linux-x64": "0.20.2",
+ "@esbuild/netbsd-x64": "0.20.2",
+ "@esbuild/openbsd-x64": "0.20.2",
+ "@esbuild/sunos-x64": "0.20.2",
+ "@esbuild/win32-arm64": "0.20.2",
+ "@esbuild/win32-ia32": "0.20.2",
+ "@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/vitest": {
diff --git a/backend/package.json b/backend/package.json
index 5c22dc237..f53ec9329 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -108,10 +108,11 @@
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
- "mysql2": "^3.9.1",
+ "mysql2": "^3.9.4",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",
+ "oracledb": "^6.4.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
diff --git a/backend/scripts/create-backend-file.ts b/backend/scripts/create-backend-file.ts
index b821aba21..fb71994ce 100644
--- a/backend/scripts/create-backend-file.ts
+++ b/backend/scripts/create-backend-file.ts
@@ -103,11 +103,15 @@ export const ${dalName} = (db: TDbClient) => {
`import { z } from "zod";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
+import { readLimit } from "@app/server/config/rateLimiter";
export const register${pascalCase}Router = async (server: FastifyZodProvider) => {
server.route({
- url: "/",
method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({}),
response: {
diff --git a/backend/scripts/create-migration.ts b/backend/scripts/create-migration.ts
index f4017b12b..59040a37a 100644
--- a/backend/scripts/create-migration.ts
+++ b/backend/scripts/create-migration.ts
@@ -7,10 +7,10 @@ const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for migration: ");
+// Remove spaces from migration name and replace with hyphens
+const formattedMigrationName = migrationName.replace(/\s+/g, "-");
+
execSync(
- `npx knex migrate:make --knexfile ${path.join(
- __dirname,
- "../src/db/knexfile.ts"
- )} -x ts ${migrationName}`,
+ `npx knex migrate:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${formattedMigrationName}`,
{ stdio: "inherit" }
);
diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 649c54c54..a4c3eea7b 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -3,9 +3,14 @@ import "fastify";
import { TUsers } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
+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";
+import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
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 { 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";
@@ -19,8 +24,9 @@ import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
-import { ActorType } from "@app/services/auth/auth-type";
+import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
+import { 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 { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@@ -59,9 +65,10 @@ declare module "fastify" {
// identity injection. depending on which kinda of token the information is filled in auth
auth: TAuthMode;
permission: {
+ authMethod: ActorAuthMethod;
type: ActorType;
id: string;
- orgId?: string;
+ orgId: string;
};
// passport data
passportUser: {
@@ -84,6 +91,8 @@ declare module "fastify" {
orgRole: TOrgRoleServiceFactory;
superAdmin: TSuperAdminServiceFactory;
user: TUserServiceFactory;
+ group: TGroupServiceFactory;
+ groupProject: TGroupProjectServiceFactory;
apiKey: TApiKeyServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;
@@ -116,6 +125,10 @@ declare module "fastify" {
trustedIp: TTrustedIpServiceFactory;
secretBlindIndex: TSecretBlindIndexServiceFactory;
telemetry: TTelemetryServiceFactory;
+ dynamicSecret: TDynamicSecretServiceFactory;
+ dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
+ projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
+ identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
};
// 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 31b80e0e3..2c8b8be5a 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -17,12 +17,27 @@ import {
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate,
+ TDynamicSecretLeases,
+ TDynamicSecretLeasesInsert,
+ TDynamicSecretLeasesUpdate,
+ TDynamicSecrets,
+ TDynamicSecretsInsert,
+ TDynamicSecretsUpdate,
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate,
TGitAppOrg,
TGitAppOrgInsert,
TGitAppOrgUpdate,
+ TGroupProjectMembershipRoles,
+ TGroupProjectMembershipRolesInsert,
+ TGroupProjectMembershipRolesUpdate,
+ TGroupProjectMemberships,
+ TGroupProjectMembershipsInsert,
+ TGroupProjectMembershipsUpdate,
+ TGroups,
+ TGroupsInsert,
+ TGroupsUpdate,
TIdentities,
TIdentitiesInsert,
TIdentitiesUpdate,
@@ -32,6 +47,9 @@ import {
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
+ TIdentityProjectAdditionalPrivilege,
+ TIdentityProjectAdditionalPrivilegeInsert,
+ TIdentityProjectAdditionalPrivilegeUpdate,
TIdentityProjectMembershipRole,
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate,
@@ -86,6 +104,9 @@ import {
TProjects,
TProjectsInsert,
TProjectsUpdate,
+ TProjectUserAdditionalPrivilege,
+ TProjectUserAdditionalPrivilegeInsert,
+ TProjectUserAdditionalPrivilegeUpdate,
TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate,
@@ -176,6 +197,9 @@ import {
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate,
+ TUserGroupMembership,
+ TUserGroupMembershipInsert,
+ TUserGroupMembershipUpdate,
TUsers,
TUsersInsert,
TUsersUpdate,
@@ -187,6 +211,22 @@ import {
declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: Knex.CompositeTableType;
+ [TableName.Groups]: Knex.CompositeTableType;
+ [TableName.UserGroupMembership]: Knex.CompositeTableType<
+ TUserGroupMembership,
+ TUserGroupMembershipInsert,
+ TUserGroupMembershipUpdate
+ >;
+ [TableName.GroupProjectMembership]: Knex.CompositeTableType<
+ TGroupProjectMemberships,
+ TGroupProjectMembershipsInsert,
+ TGroupProjectMembershipsUpdate
+ >;
+ [TableName.GroupProjectMembershipRole]: Knex.CompositeTableType<
+ TGroupProjectMembershipRoles,
+ TGroupProjectMembershipRolesInsert,
+ TGroupProjectMembershipRolesUpdate
+ >;
[TableName.UserAliases]: Knex.CompositeTableType;
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
TUserEncryptionKeys,
@@ -233,6 +273,11 @@ declare module "knex/types/tables" {
TProjectUserMembershipRolesUpdate
>;
[TableName.ProjectRoles]: Knex.CompositeTableType;
+ [TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
+ TProjectUserAdditionalPrivilege,
+ TProjectUserAdditionalPrivilegeInsert,
+ TProjectUserAdditionalPrivilegeUpdate
+ >;
[TableName.ProjectKeys]: Knex.CompositeTableType;
[TableName.Secret]: Knex.CompositeTableType;
[TableName.SecretBlindIndex]: Knex.CompositeTableType<
@@ -288,6 +333,11 @@ declare module "knex/types/tables" {
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate
>;
+ [TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType<
+ TIdentityProjectAdditionalPrivilege,
+ TIdentityProjectAdditionalPrivilegeInsert,
+ TIdentityProjectAdditionalPrivilegeUpdate
+ >;
[TableName.ScimToken]: Knex.CompositeTableType;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,
@@ -340,6 +390,12 @@ declare module "knex/types/tables" {
TSecretSnapshotFoldersInsert,
TSecretSnapshotFoldersUpdate
>;
+ [TableName.DynamicSecret]: Knex.CompositeTableType;
+ [TableName.DynamicSecretLease]: Knex.CompositeTableType<
+ TDynamicSecretLeases,
+ TDynamicSecretLeasesInsert,
+ TDynamicSecretLeasesUpdate
+ >;
[TableName.SamlConfig]: Knex.CompositeTableType;
[TableName.LdapConfig]: Knex.CompositeTableType;
[TableName.OrgBot]: Knex.CompositeTableType;
diff --git a/backend/src/db/migrations/20240318164718_dynamic-secret.ts b/backend/src/db/migrations/20240318164718_dynamic-secret.ts
new file mode 100644
index 000000000..743744a03
--- /dev/null
+++ b/backend/src/db/migrations/20240318164718_dynamic-secret.ts
@@ -0,0 +1,58 @@
+import { Knex } from "knex";
+
+import { SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ const doesTableExist = await knex.schema.hasTable(TableName.DynamicSecret);
+ if (!doesTableExist) {
+ await knex.schema.createTable(TableName.DynamicSecret, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.string("name").notNullable();
+ t.integer("version").notNullable();
+ t.string("type").notNullable();
+ t.string("defaultTTL").notNullable();
+ t.string("maxTTL");
+ t.string("inputIV").notNullable();
+ t.text("inputCiphertext").notNullable();
+ t.string("inputTag").notNullable();
+ t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
+ t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
+ t.uuid("folderId").notNullable();
+ // for background process communication
+ t.string("status");
+ t.string("statusDetails");
+ t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
+ t.unique(["name", "folderId"]);
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.DynamicSecret);
+
+ const doesTableDynamicSecretLease = await knex.schema.hasTable(TableName.DynamicSecretLease);
+ if (!doesTableDynamicSecretLease) {
+ await knex.schema.createTable(TableName.DynamicSecretLease, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.integer("version").notNullable();
+ t.string("externalEntityId").notNullable();
+ t.datetime("expireAt").notNullable();
+ // for background process communication
+ t.string("status");
+ t.string("statusDetails");
+ t.uuid("dynamicSecretId").notNullable();
+ t.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.DynamicSecretLease);
+}
+
+export async function down(knex: Knex): Promise {
+ await dropOnUpdateTrigger(knex, TableName.DynamicSecretLease);
+ await knex.schema.dropTableIfExists(TableName.DynamicSecretLease);
+
+ await dropOnUpdateTrigger(knex, TableName.DynamicSecret);
+ await knex.schema.dropTableIfExists(TableName.DynamicSecret);
+}
diff --git a/backend/src/db/migrations/20240326172010_project-user-additional-privilege.ts b/backend/src/db/migrations/20240326172010_project-user-additional-privilege.ts
new file mode 100644
index 000000000..0366ba507
--- /dev/null
+++ b/backend/src/db/migrations/20240326172010_project-user-additional-privilege.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.ProjectUserAdditionalPrivilege))) {
+ await knex.schema.createTable(TableName.ProjectUserAdditionalPrivilege, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.string("slug", 60).notNullable();
+ t.uuid("projectMembershipId").notNullable();
+ t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
+ t.boolean("isTemporary").notNullable().defaultTo(false);
+ t.string("temporaryMode");
+ t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
+ t.datetime("temporaryAccessStartTime");
+ t.datetime("temporaryAccessEndTime");
+ t.jsonb("permissions").notNullable();
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege);
+}
+
+export async function down(knex: Knex): Promise {
+ await dropOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege);
+ await knex.schema.dropTableIfExists(TableName.ProjectUserAdditionalPrivilege);
+}
diff --git a/backend/src/db/migrations/20240326172011_machine-identity-additional-privilege.ts b/backend/src/db/migrations/20240326172011_machine-identity-additional-privilege.ts
new file mode 100644
index 000000000..c59fc685a
--- /dev/null
+++ b/backend/src/db/migrations/20240326172011_machine-identity-additional-privilege.ts
@@ -0,0 +1,32 @@
+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.IdentityProjectAdditionalPrivilege))) {
+ await knex.schema.createTable(TableName.IdentityProjectAdditionalPrivilege, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.string("slug", 60).notNullable();
+ t.uuid("projectMembershipId").notNullable();
+ t.foreign("projectMembershipId")
+ .references("id")
+ .inTable(TableName.IdentityProjectMembership)
+ .onDelete("CASCADE");
+ t.boolean("isTemporary").notNullable().defaultTo(false);
+ t.string("temporaryMode");
+ t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
+ t.datetime("temporaryAccessStartTime");
+ t.datetime("temporaryAccessEndTime");
+ t.jsonb("permissions").notNullable();
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege);
+}
+
+export async function down(knex: Knex): Promise {
+ await dropOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege);
+ await knex.schema.dropTableIfExists(TableName.IdentityProjectAdditionalPrivilege);
+}
diff --git a/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts b/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts
new file mode 100644
index 000000000..9a342d542
--- /dev/null
+++ b/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts
@@ -0,0 +1,111 @@
+import { Knex } from "knex";
+import { z } from "zod";
+
+import { TableName, TOrgMemberships } from "../schemas";
+
+const validateOrgMembership = (membershipToValidate: TOrgMemberships, firstMembership: TOrgMemberships) => {
+ const firstOrgId = firstMembership.orgId;
+ const firstUserId = firstMembership.userId;
+
+ if (membershipToValidate.id === firstMembership.id) {
+ return;
+ }
+
+ if (membershipToValidate.inviteEmail !== firstMembership.inviteEmail) {
+ throw new Error(`Invite emails are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
+ }
+ if (membershipToValidate.orgId !== firstMembership.orgId) {
+ throw new Error(`OrgIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
+ }
+ if (membershipToValidate.role !== firstMembership.role) {
+ throw new Error(`Roles are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
+ }
+ if (membershipToValidate.roleId !== firstMembership.roleId) {
+ throw new Error(`RoleIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
+ }
+ if (membershipToValidate.status !== firstMembership.status) {
+ throw new Error(`Statuses are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
+ }
+ if (membershipToValidate.userId !== firstMembership.userId) {
+ throw new Error(`UserIds are different for the same userId and orgId: ${firstUserId}, ${firstOrgId}`);
+ }
+};
+
+export async function up(knex: Knex): Promise {
+ const RowSchema = z.object({
+ userId: z.string(),
+ orgId: z.string(),
+ cnt: z.string()
+ });
+
+ // Transactional find and delete duplicate rows
+ await knex.transaction(async (tx) => {
+ const duplicateRows = await tx(TableName.OrgMembership)
+ .select("userId", "orgId") // Select the userId and orgId so we can group by them
+ .count("* as cnt") // Count the number of rows for each userId and orgId, so we can make sure there are more than 1 row (a duplicate)
+ .groupBy("userId", "orgId")
+ .havingRaw("count(*) > ?", [1]); // Using havingRaw for direct SQL expressions
+
+ // Parse the rows to ensure they are in the correct format, and for type safety
+ const parsedRows = RowSchema.array().parse(duplicateRows);
+
+ // For each of the duplicate rows, loop through and find the actual memberships to delete
+ for (const row of parsedRows) {
+ const count = Number(row.cnt);
+
+ // An extra check to ensure that the count is actually a number, and the number is greater than 2
+ if (typeof count !== "number" || count < 2) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ // Find all the organization memberships that have the same userId and orgId
+ // eslint-disable-next-line no-await-in-loop
+ const rowsToDelete = await tx(TableName.OrgMembership).where({
+ userId: row.userId,
+ orgId: row.orgId
+ });
+
+ // Ensure that all the rows have exactly the same value, except id, createdAt, updatedAt
+ for (const rowToDelete of rowsToDelete) {
+ validateOrgMembership(rowToDelete, rowsToDelete[0]);
+ }
+
+ // Find the row with the latest createdAt, which we will keep
+
+ let lowestCreatedAt: number | null = null;
+ let latestCreatedRow: TOrgMemberships | null = null;
+
+ for (const rowToDelete of rowsToDelete) {
+ if (lowestCreatedAt === null || rowToDelete.createdAt.getTime() < lowestCreatedAt) {
+ lowestCreatedAt = rowToDelete.createdAt.getTime();
+ latestCreatedRow = rowToDelete;
+ }
+ }
+ if (!latestCreatedRow) {
+ throw new Error("Failed to find last created membership");
+ }
+
+ // Filter out the latest row from the rows to delete
+ const membershipIdsToDelete = rowsToDelete.map((r) => r.id).filter((id) => id !== latestCreatedRow!.id);
+
+ // eslint-disable-next-line no-await-in-loop
+ const numberOfRowsDeleted = await tx(TableName.OrgMembership).whereIn("id", membershipIdsToDelete).delete();
+
+ // eslint-disable-next-line no-console
+ console.log(
+ `Deleted ${numberOfRowsDeleted} duplicate organization memberships for ${row.userId} and ${row.orgId}`
+ );
+ }
+ });
+
+ await knex.schema.alterTable(TableName.OrgMembership, (table) => {
+ table.unique(["userId", "orgId"]);
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.alterTable(TableName.OrgMembership, (table) => {
+ table.dropUnique(["userId", "orgId"]);
+ });
+}
diff --git a/backend/src/db/migrations/20240412174842_group.ts b/backend/src/db/migrations/20240412174842_group.ts
new file mode 100644
index 000000000..53014dc53
--- /dev/null
+++ b/backend/src/db/migrations/20240412174842_group.ts
@@ -0,0 +1,82 @@
+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.Groups))) {
+ await knex.schema.createTable(TableName.Groups, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.uuid("orgId").notNullable();
+ t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
+ t.string("name").notNullable();
+ t.string("slug").notNullable();
+ t.unique(["orgId", "slug"]);
+ t.string("role").notNullable();
+ t.uuid("roleId");
+ t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.Groups);
+
+ if (!(await knex.schema.hasTable(TableName.UserGroupMembership))) {
+ await knex.schema.createTable(TableName.UserGroupMembership, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); // link to user and link to groups cascade on groups
+ t.uuid("userId").notNullable();
+ t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
+ t.uuid("groupId").notNullable();
+ t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.UserGroupMembership);
+
+ if (!(await knex.schema.hasTable(TableName.GroupProjectMembership))) {
+ await knex.schema.createTable(TableName.GroupProjectMembership, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.string("projectId").notNullable();
+ t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
+ t.uuid("groupId").notNullable();
+ t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
+ t.timestamps(true, true, true);
+ });
+ }
+ await createOnUpdateTrigger(knex, TableName.GroupProjectMembership);
+
+ if (!(await knex.schema.hasTable(TableName.GroupProjectMembershipRole))) {
+ await knex.schema.createTable(TableName.GroupProjectMembershipRole, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.string("role").notNullable();
+ t.uuid("projectMembershipId").notNullable();
+ t.foreign("projectMembershipId").references("id").inTable(TableName.GroupProjectMembership).onDelete("CASCADE");
+ // until role is changed/removed the role should not deleted
+ t.uuid("customRoleId");
+ t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles);
+ t.boolean("isTemporary").notNullable().defaultTo(false);
+ t.string("temporaryMode");
+ t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc
+ t.datetime("temporaryAccessStartTime");
+ t.datetime("temporaryAccessEndTime");
+ t.timestamps(true, true, true);
+ });
+ }
+
+ await createOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.dropTableIfExists(TableName.GroupProjectMembershipRole);
+ await dropOnUpdateTrigger(knex, TableName.GroupProjectMembershipRole);
+
+ await knex.schema.dropTableIfExists(TableName.UserGroupMembership);
+ await dropOnUpdateTrigger(knex, TableName.UserGroupMembership);
+
+ await knex.schema.dropTableIfExists(TableName.GroupProjectMembership);
+ await dropOnUpdateTrigger(knex, TableName.GroupProjectMembership);
+
+ await knex.schema.dropTableIfExists(TableName.Groups);
+ await dropOnUpdateTrigger(knex, TableName.Groups);
+}
diff --git a/backend/src/db/migrations/20240414192520_drop-role-roleid-project-membership.ts b/backend/src/db/migrations/20240414192520_drop-role-roleid-project-membership.ts
new file mode 100644
index 000000000..2dd58c5d1
--- /dev/null
+++ b/backend/src/db/migrations/20240414192520_drop-role-roleid-project-membership.ts
@@ -0,0 +1,47 @@
+import { Knex } from "knex";
+
+import { ProjectMembershipRole, TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const doesProjectRoleFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "role");
+ const doesProjectRoleIdFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "roleId");
+ await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
+ if (doesProjectRoleFieldExist) t.dropColumn("roleId");
+ if (doesProjectRoleIdFieldExist) t.dropColumn("role");
+ });
+
+ const doesIdentityProjectRoleFieldExist = await knex.schema.hasColumn(TableName.IdentityProjectMembership, "role");
+ const doesIdentityProjectRoleIdFieldExist = await knex.schema.hasColumn(
+ TableName.IdentityProjectMembership,
+ "roleId"
+ );
+ await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
+ if (doesIdentityProjectRoleFieldExist) t.dropColumn("roleId");
+ if (doesIdentityProjectRoleIdFieldExist) t.dropColumn("role");
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ const doesProjectRoleFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "role");
+ const doesProjectRoleIdFieldExist = await knex.schema.hasColumn(TableName.ProjectMembership, "roleId");
+ await knex.schema.alterTable(TableName.ProjectMembership, (t) => {
+ if (!doesProjectRoleFieldExist) t.string("role").defaultTo(ProjectMembershipRole.Member);
+ if (!doesProjectRoleIdFieldExist) {
+ t.uuid("roleId");
+ t.foreign("roleId").references("id").inTable(TableName.ProjectRoles);
+ }
+ });
+
+ const doesIdentityProjectRoleFieldExist = await knex.schema.hasColumn(TableName.IdentityProjectMembership, "role");
+ const doesIdentityProjectRoleIdFieldExist = await knex.schema.hasColumn(
+ TableName.IdentityProjectMembership,
+ "roleId"
+ );
+ await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => {
+ if (!doesIdentityProjectRoleFieldExist) t.string("role").defaultTo(ProjectMembershipRole.Member);
+ if (!doesIdentityProjectRoleIdFieldExist) {
+ t.uuid("roleId");
+ t.foreign("roleId").references("id").inTable(TableName.ProjectRoles);
+ }
+ });
+}
diff --git a/backend/src/db/schemas/dynamic-secret-leases.ts b/backend/src/db/schemas/dynamic-secret-leases.ts
new file mode 100644
index 000000000..8c16bcb55
--- /dev/null
+++ b/backend/src/db/schemas/dynamic-secret-leases.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 { TImmutableDBKeys } from "./models";
+
+export const DynamicSecretLeasesSchema = z.object({
+ id: z.string().uuid(),
+ version: z.number(),
+ externalEntityId: z.string(),
+ expireAt: z.date(),
+ status: z.string().nullable().optional(),
+ statusDetails: z.string().nullable().optional(),
+ dynamicSecretId: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TDynamicSecretLeases = z.infer;
+export type TDynamicSecretLeasesInsert = Omit, TImmutableDBKeys>;
+export type TDynamicSecretLeasesUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts
new file mode 100644
index 000000000..b27da396c
--- /dev/null
+++ b/backend/src/db/schemas/dynamic-secrets.ts
@@ -0,0 +1,31 @@
+// 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 DynamicSecretsSchema = z.object({
+ id: z.string().uuid(),
+ name: z.string(),
+ version: z.number(),
+ type: z.string(),
+ defaultTTL: z.string(),
+ maxTTL: z.string().nullable().optional(),
+ inputIV: z.string(),
+ inputCiphertext: z.string(),
+ inputTag: z.string(),
+ algorithm: z.string().default("aes-256-gcm"),
+ keyEncoding: z.string().default("utf8"),
+ folderId: z.string().uuid(),
+ status: z.string().nullable().optional(),
+ statusDetails: z.string().nullable().optional(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TDynamicSecrets = z.infer;
+export type TDynamicSecretsInsert = Omit, TImmutableDBKeys>;
+export type TDynamicSecretsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/group-project-membership-roles.ts b/backend/src/db/schemas/group-project-membership-roles.ts
new file mode 100644
index 000000000..d837ca8e7
--- /dev/null
+++ b/backend/src/db/schemas/group-project-membership-roles.ts
@@ -0,0 +1,31 @@
+// 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 GroupProjectMembershipRolesSchema = z.object({
+ id: z.string().uuid(),
+ role: z.string(),
+ projectMembershipId: z.string().uuid(),
+ customRoleId: z.string().uuid().nullable().optional(),
+ isTemporary: z.boolean().default(false),
+ temporaryMode: z.string().nullable().optional(),
+ temporaryRange: z.string().nullable().optional(),
+ temporaryAccessStartTime: z.date().nullable().optional(),
+ temporaryAccessEndTime: z.date().nullable().optional(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TGroupProjectMembershipRoles = z.infer;
+export type TGroupProjectMembershipRolesInsert = Omit<
+ z.input,
+ TImmutableDBKeys
+>;
+export type TGroupProjectMembershipRolesUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/group-project-memberships.ts b/backend/src/db/schemas/group-project-memberships.ts
new file mode 100644
index 000000000..7787a3574
--- /dev/null
+++ b/backend/src/db/schemas/group-project-memberships.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 { TImmutableDBKeys } from "./models";
+
+export const GroupProjectMembershipsSchema = z.object({
+ id: z.string().uuid(),
+ projectId: z.string(),
+ groupId: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TGroupProjectMemberships = z.infer;
+export type TGroupProjectMembershipsInsert = Omit, TImmutableDBKeys>;
+export type TGroupProjectMembershipsUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/groups.ts b/backend/src/db/schemas/groups.ts
new file mode 100644
index 000000000..9733d253e
--- /dev/null
+++ b/backend/src/db/schemas/groups.ts
@@ -0,0 +1,23 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const GroupsSchema = z.object({
+ id: z.string().uuid(),
+ orgId: z.string().uuid(),
+ name: z.string(),
+ slug: z.string(),
+ role: z.string(),
+ roleId: z.string().uuid().nullable().optional(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TGroups = z.infer;
+export type TGroupsInsert = Omit, TImmutableDBKeys>;
+export type TGroupsUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/schemas/identity-project-additional-privilege.ts b/backend/src/db/schemas/identity-project-additional-privilege.ts
new file mode 100644
index 000000000..7a9dbe19e
--- /dev/null
+++ b/backend/src/db/schemas/identity-project-additional-privilege.ts
@@ -0,0 +1,31 @@
+// 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 IdentityProjectAdditionalPrivilegeSchema = z.object({
+ id: z.string().uuid(),
+ slug: z.string(),
+ projectMembershipId: z.string().uuid(),
+ isTemporary: z.boolean().default(false),
+ temporaryMode: z.string().nullable().optional(),
+ temporaryRange: z.string().nullable().optional(),
+ temporaryAccessStartTime: z.date().nullable().optional(),
+ temporaryAccessEndTime: z.date().nullable().optional(),
+ permissions: z.unknown(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TIdentityProjectAdditionalPrivilege = z.infer;
+export type TIdentityProjectAdditionalPrivilegeInsert = Omit<
+ z.input,
+ TImmutableDBKeys
+>;
+export type TIdentityProjectAdditionalPrivilegeUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/identity-project-memberships.ts b/backend/src/db/schemas/identity-project-memberships.ts
index 276c9581e..2f17c36d8 100644
--- a/backend/src/db/schemas/identity-project-memberships.ts
+++ b/backend/src/db/schemas/identity-project-memberships.ts
@@ -9,8 +9,6 @@ import { TImmutableDBKeys } from "./models";
export const IdentityProjectMembershipsSchema = z.object({
id: z.string().uuid(),
- role: z.string(),
- roleId: z.string().uuid().nullable().optional(),
projectId: z.string(),
identityId: z.string().uuid(),
createdAt: z.date(),
diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts
index 001fdbf18..b9dab06ba 100644
--- a/backend/src/db/schemas/index.ts
+++ b/backend/src/db/schemas/index.ts
@@ -3,11 +3,17 @@ export * from "./audit-logs";
export * from "./auth-token-sessions";
export * from "./auth-tokens";
export * from "./backup-private-key";
+export * from "./dynamic-secret-leases";
+export * from "./dynamic-secrets";
export * from "./git-app-install-sessions";
export * from "./git-app-org";
+export * from "./group-project-membership-roles";
+export * from "./group-project-memberships";
+export * from "./groups";
export * from "./identities";
export * from "./identity-access-tokens";
export * from "./identity-org-memberships";
+export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role";
export * from "./identity-project-memberships";
export * from "./identity-ua-client-secrets";
@@ -26,6 +32,7 @@ export * from "./project-environments";
export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
+export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";
export * from "./saml-configs";
@@ -57,5 +64,6 @@ export * from "./trusted-ips";
export * from "./user-actions";
export * from "./user-aliases";
export * from "./user-encryption-keys";
+export * from "./user-group-membership";
export * from "./users";
export * from "./webhooks";
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index f85feff9c..d5cf1b886 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -2,6 +2,10 @@ import { z } from "zod";
export enum TableName {
Users = "users",
+ Groups = "groups",
+ GroupProjectMembership = "group_project_memberships",
+ GroupProjectMembershipRole = "group_project_membership_roles",
+ UserGroupMembership = "user_group_membership",
UserAliases = "user_aliases",
UserEncryptionKey = "user_encryption_keys",
AuthTokens = "auth_tokens",
@@ -20,6 +24,7 @@ export enum TableName {
Environment = "project_environments",
ProjectMembership = "project_memberships",
ProjectRoles = "project_roles",
+ ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys",
Secret = "secrets",
@@ -43,6 +48,7 @@ export enum TableName {
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
+ IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
ScimToken = "scim_tokens",
SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
@@ -59,6 +65,8 @@ export enum TableName {
GitAppOrg = "git_app_org",
SecretScanningGitRisk = "secret_scanning_git_risks",
TrustedIps = "trusted_ips",
+ DynamicSecret = "dynamic_secrets",
+ DynamicSecretLease = "dynamic_secret_leases",
// junction tables with tags
JnSecretTag = "secret_tag_junction",
SecretVersionTag = "secret_version_tag_junction"
diff --git a/backend/src/db/schemas/project-memberships.ts b/backend/src/db/schemas/project-memberships.ts
index 8576a318e..e522d6280 100644
--- a/backend/src/db/schemas/project-memberships.ts
+++ b/backend/src/db/schemas/project-memberships.ts
@@ -9,12 +9,10 @@ import { TImmutableDBKeys } from "./models";
export const ProjectMembershipsSchema = z.object({
id: z.string().uuid(),
- role: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
userId: z.string().uuid(),
- projectId: z.string(),
- roleId: z.string().uuid().nullable().optional()
+ projectId: z.string()
});
export type TProjectMemberships = z.infer;
diff --git a/backend/src/db/schemas/project-user-additional-privilege.ts b/backend/src/db/schemas/project-user-additional-privilege.ts
new file mode 100644
index 000000000..0fd0e5faa
--- /dev/null
+++ b/backend/src/db/schemas/project-user-additional-privilege.ts
@@ -0,0 +1,31 @@
+// 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 ProjectUserAdditionalPrivilegeSchema = z.object({
+ id: z.string().uuid(),
+ slug: z.string(),
+ projectMembershipId: z.string().uuid(),
+ isTemporary: z.boolean().default(false),
+ temporaryMode: z.string().nullable().optional(),
+ temporaryRange: z.string().nullable().optional(),
+ temporaryAccessStartTime: z.date().nullable().optional(),
+ temporaryAccessEndTime: z.date().nullable().optional(),
+ permissions: z.unknown(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TProjectUserAdditionalPrivilege = z.infer;
+export type TProjectUserAdditionalPrivilegeInsert = Omit<
+ z.input,
+ TImmutableDBKeys
+>;
+export type TProjectUserAdditionalPrivilegeUpdate = Partial<
+ Omit, TImmutableDBKeys>
+>;
diff --git a/backend/src/db/schemas/user-group-membership.ts b/backend/src/db/schemas/user-group-membership.ts
new file mode 100644
index 000000000..b6345d85a
--- /dev/null
+++ b/backend/src/db/schemas/user-group-membership.ts
@@ -0,0 +1,20 @@
+// Code generated by automation script, DO NOT EDIT.
+// Automated by pulling database and generating zod schema
+// To update. Just run npm run generate:schema
+// Written by akhilmhdh.
+
+import { z } from "zod";
+
+import { TImmutableDBKeys } from "./models";
+
+export const UserGroupMembershipSchema = z.object({
+ id: z.string().uuid(),
+ userId: z.string().uuid(),
+ groupId: z.string().uuid(),
+ createdAt: z.date(),
+ updatedAt: z.date()
+});
+
+export type TUserGroupMembership = z.infer;
+export type TUserGroupMembershipInsert = Omit, TImmutableDBKeys>;
+export type TUserGroupMembershipUpdate = Partial, TImmutableDBKeys>>;
diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts
index d41efb71c..934130494 100644
--- a/backend/src/db/seeds/3-project.ts
+++ b/backend/src/db/seeds/3-project.ts
@@ -33,8 +33,7 @@ export async function seed(knex: Knex): Promise {
const projectMembership = await knex(TableName.ProjectMembership)
.insert({
projectId: project.id,
- userId: seedData1.id,
- role: ProjectMembershipRole.Admin
+ userId: seedData1.id
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
diff --git a/backend/src/db/seeds/4-machine-identity.ts b/backend/src/db/seeds/4-machine-identity.ts
index 618c47114..662232e02 100644
--- a/backend/src/db/seeds/4-machine-identity.ts
+++ b/backend/src/db/seeds/4-machine-identity.ts
@@ -78,8 +78,7 @@ export async function seed(knex: Knex): Promise {
const identityProjectMembership = await knex(TableName.IdentityProjectMembership)
.insert({
identityId: seedData1.machineIdentity.id,
- projectId: seedData1.project.id,
- role: ProjectMembershipRole.Admin
+ projectId: seedData1.project.id
})
.returning("*");
diff --git a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts
new file mode 100644
index 000000000..5ef9f7eeb
--- /dev/null
+++ b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts
@@ -0,0 +1,197 @@
+import ms from "ms";
+import { z } from "zod";
+
+import { DynamicSecretLeasesSchema } from "@app/db/schemas";
+import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
+import { daysToMillisecond } from "@app/lib/dates";
+import { removeTrailingSlash } from "@app/lib/fn";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ body: z.object({
+ dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(),
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug),
+ ttl: z
+ .string()
+ .optional()
+ .describe(DYNAMIC_SECRET_LEASES.CREATE.ttl)
+ .superRefine((val, ctx) => {
+ if (!val) return;
+ const valMs = ms(val);
+ if (valMs < 60 * 1000)
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
+ if (valMs > daysToMillisecond(1))
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
+ }),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.path)
+ }),
+ response: {
+ 200: z.object({
+ lease: DynamicSecretLeasesSchema,
+ dynamicSecret: SanitizedDynamicSecretSchema,
+ data: z.unknown()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const { data, lease, dynamicSecret } = await server.services.dynamicSecretLease.create({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ name: req.body.dynamicSecretName,
+ ...req.body
+ });
+ return { lease, data, dynamicSecret };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:leaseId",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId)
+ }),
+ body: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.projectSlug),
+ path: z
+ .string()
+ .min(1)
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(DYNAMIC_SECRET_LEASES.DELETE.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.environmentSlug),
+ isForced: z.boolean().default(false).describe(DYNAMIC_SECRET_LEASES.DELETE.isForced)
+ }),
+ response: {
+ 200: z.object({
+ lease: DynamicSecretLeasesSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const lease = await server.services.dynamicSecretLease.revokeLease({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ leaseId: req.params.leaseId,
+ ...req.body
+ });
+ return { lease };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/:leaseId/renew",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId)
+ }),
+ body: z.object({
+ ttl: z
+ .string()
+ .describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
+ .optional()
+ .superRefine((val, ctx) => {
+ if (!val) return;
+ const valMs = ms(val);
+ if (valMs < 60 * 1000)
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
+ if (valMs > daysToMillisecond(1))
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
+ }),
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
+ path: z
+ .string()
+ .min(1)
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(DYNAMIC_SECRET_LEASES.RENEW.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
+ }),
+ response: {
+ 200: z.object({
+ lease: DynamicSecretLeasesSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const lease = await server.services.dynamicSecretLease.renewLease({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ leaseId: req.params.leaseId,
+ ...req.body
+ });
+ return { lease };
+ }
+ });
+
+ server.route({
+ url: "/:leaseId",
+ method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId)
+ }),
+ querystring: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.projectSlug),
+ path: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.environmentSlug)
+ }),
+ response: {
+ 200: z.object({
+ lease: DynamicSecretLeasesSchema.extend({
+ dynamicSecret: SanitizedDynamicSecretSchema
+ })
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const lease = await server.services.dynamicSecretLease.getLeaseDetails({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ leaseId: req.params.leaseId,
+ ...req.query
+ });
+ return { lease };
+ }
+ });
+};
diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts
new file mode 100644
index 000000000..049370743
--- /dev/null
+++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts
@@ -0,0 +1,290 @@
+import slugify from "@sindresorhus/slugify";
+import ms from "ms";
+import { z } from "zod";
+
+import { DynamicSecretLeasesSchema } from "@app/db/schemas";
+import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
+import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
+import { daysToMillisecond } from "@app/lib/dates";
+import { removeTrailingSlash } from "@app/lib/fn";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ body: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug),
+ provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider),
+ defaultTTL: z
+ .string()
+ .describe(DYNAMIC_SECRETS.CREATE.defaultTTL)
+ .superRefine((val, ctx) => {
+ const valMs = ms(val);
+ if (valMs < 60 * 1000)
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
+ if (valMs > daysToMillisecond(1))
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
+ }),
+ maxTTL: z
+ .string()
+ .describe(DYNAMIC_SECRETS.CREATE.maxTTL)
+ .optional()
+ .superRefine((val, ctx) => {
+ if (!val) return;
+ const valMs = ms(val);
+ if (valMs < 60 * 1000)
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
+ if (valMs > daysToMillisecond(1))
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
+ })
+ .nullable(),
+ path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
+ environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
+ name: z
+ .string()
+ .describe(DYNAMIC_SECRETS.CREATE.name)
+ .min(1)
+ .toLowerCase()
+ .max(64)
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid"
+ })
+ }),
+ response: {
+ 200: z.object({
+ dynamicSecret: SanitizedDynamicSecretSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const dynamicSecretCfg = await server.services.dynamicSecret.create({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+ return { dynamicSecret: dynamicSecretCfg };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/:name",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name)
+ }),
+ body: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.UPDATE.projectSlug),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.UPDATE.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.UPDATE.environmentSlug),
+ data: z.object({
+ inputs: z.any().optional().describe(DYNAMIC_SECRETS.UPDATE.inputs),
+ defaultTTL: z
+ .string()
+ .describe(DYNAMIC_SECRETS.UPDATE.defaultTTL)
+ .optional()
+ .superRefine((val, ctx) => {
+ if (!val) return;
+ const valMs = ms(val);
+ if (valMs < 60 * 1000)
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
+ if (valMs > daysToMillisecond(1))
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
+ }),
+ maxTTL: z
+ .string()
+ .describe(DYNAMIC_SECRETS.UPDATE.maxTTL)
+ .optional()
+ .superRefine((val, ctx) => {
+ if (!val) return;
+ const valMs = ms(val);
+ if (valMs < 60 * 1000)
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
+ if (valMs > daysToMillisecond(1))
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
+ })
+ .nullable(),
+ newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
+ })
+ }),
+ response: {
+ 200: z.object({
+ dynamicSecret: SanitizedDynamicSecretSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const dynamicSecretCfg = await server.services.dynamicSecret.updateByName({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ name: req.params.name,
+ path: req.body.path,
+ projectSlug: req.body.projectSlug,
+ environmentSlug: req.body.environmentSlug,
+ ...req.body.data
+ });
+ return { dynamicSecret: dynamicSecretCfg };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:name",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name)
+ }),
+ body: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.DELETE.projectSlug),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.DELETE.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.DELETE.environmentSlug),
+ isForced: z.boolean().default(false).describe(DYNAMIC_SECRETS.DELETE.isForced)
+ }),
+ response: {
+ 200: z.object({
+ dynamicSecret: SanitizedDynamicSecretSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const dynamicSecretCfg = await server.services.dynamicSecret.deleteByName({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ name: req.params.name,
+ ...req.body
+ });
+ return { dynamicSecret: dynamicSecretCfg };
+ }
+ });
+
+ server.route({
+ url: "/:name",
+ method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name)
+ }),
+ querystring: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.projectSlug),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.GET_BY_NAME.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.environmentSlug)
+ }),
+ response: {
+ 200: z.object({
+ dynamicSecret: SanitizedDynamicSecretSchema.extend({
+ inputs: z.unknown()
+ })
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const dynamicSecretCfg = await server.services.dynamicSecret.getDetails({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ name: req.params.name,
+ ...req.query
+ });
+ return { dynamicSecret: dynamicSecretCfg };
+ }
+ });
+
+ server.route({
+ url: "/",
+ method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ querystring: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.LIST.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.environmentSlug)
+ }),
+ response: {
+ 200: z.object({
+ dynamicSecrets: SanitizedDynamicSecretSchema.array()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const dynamicSecretCfgs = await server.services.dynamicSecret.list({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.query
+ });
+ return { dynamicSecrets: dynamicSecretCfgs };
+ }
+ });
+
+ server.route({
+ url: "/:name/leases",
+ method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name)
+ }),
+ querystring: z.object({
+ projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.projectSlug),
+ path: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.path),
+ environmentSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.environmentSlug)
+ }),
+ response: {
+ 200: z.object({
+ leases: DynamicSecretLeasesSchema.array()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const leases = await server.services.dynamicSecretLease.listLeases({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ name: req.params.name,
+ ...req.query
+ });
+ return { leases };
+ }
+ });
+};
diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts
new file mode 100644
index 000000000..d267564f2
--- /dev/null
+++ b/backend/src/ee/routes/v1/group-router.ts
@@ -0,0 +1,220 @@
+import slugify from "@sindresorhus/slugify";
+import { z } from "zod";
+
+import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
+import { GROUPS } from "@app/lib/api-docs";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerGroupRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ url: "/",
+ method: "POST",
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ body: z.object({
+ name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
+ slug: z
+ .string()
+ .min(5)
+ .max(36)
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .optional()
+ .describe(GROUPS.CREATE.slug),
+ role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
+ }),
+ response: {
+ 200: GroupsSchema
+ }
+ },
+ handler: async (req) => {
+ const group = await server.services.group.createGroup({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ return group;
+ }
+ });
+
+ server.route({
+ url: "/:currentSlug",
+ method: "PATCH",
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
+ }),
+ body: z
+ .object({
+ name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
+ slug: z
+ .string()
+ .min(5)
+ .max(36)
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .describe(GROUPS.UPDATE.slug),
+ role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
+ })
+ .partial(),
+ response: {
+ 200: GroupsSchema
+ }
+ },
+ handler: async (req) => {
+ const group = await server.services.group.updateGroup({
+ currentSlug: req.params.currentSlug,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.body
+ });
+
+ return group;
+ }
+ });
+
+ server.route({
+ url: "/:slug",
+ method: "DELETE",
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ slug: z.string().trim().describe(GROUPS.DELETE.slug)
+ }),
+ response: {
+ 200: GroupsSchema
+ }
+ },
+ handler: async (req) => {
+ const group = await server.services.group.deleteGroup({
+ groupSlug: req.params.slug,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ return group;
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:slug/users",
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
+ }),
+ querystring: z.object({
+ offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
+ limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
+ username: z.string().optional().describe(GROUPS.LIST_USERS.username)
+ }),
+ response: {
+ 200: z.object({
+ users: UsersSchema.pick({
+ email: true,
+ username: true,
+ firstName: true,
+ lastName: true,
+ id: true
+ })
+ .merge(
+ z.object({
+ isPartOfGroup: z.boolean()
+ })
+ )
+ .array(),
+ totalCount: z.number()
+ })
+ }
+ },
+ handler: async (req) => {
+ const { users, totalCount } = await server.services.group.listGroupUsers({
+ groupSlug: req.params.slug,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.query
+ });
+ return { users, totalCount };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/:slug/users/:username",
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
+ username: z.string().trim().describe(GROUPS.ADD_USER.username)
+ }),
+ response: {
+ 200: UsersSchema.pick({
+ email: true,
+ username: true,
+ firstName: true,
+ lastName: true,
+ id: true
+ })
+ }
+ },
+ handler: async (req) => {
+ const user = await server.services.group.addUserToGroup({
+ groupSlug: req.params.slug,
+ username: req.params.username,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ return user;
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:slug/users/:username",
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
+ username: z.string().trim().describe(GROUPS.DELETE_USER.username)
+ }),
+ response: {
+ 200: UsersSchema.pick({
+ email: true,
+ username: true,
+ firstName: true,
+ lastName: true,
+ id: true
+ })
+ }
+ },
+ handler: async (req) => {
+ const user = await server.services.group.removeUserFromGroup({
+ groupSlug: req.params.slug,
+ username: req.params.username,
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ return user;
+ }
+ });
+};
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
new file mode 100644
index 000000000..a1a2e36fa
--- /dev/null
+++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts
@@ -0,0 +1,329 @@
+import { MongoAbility, RawRuleOf } from "@casl/ability";
+import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
+import slugify from "@sindresorhus/slugify";
+import ms from "ms";
+import { z } from "zod";
+
+import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas";
+import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
+import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission";
+import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
+import { alphaNumericNanoId } from "@app/lib/nanoid";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/permanent",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Create a permanent or a non expiry specific privilege for identity.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ body: z.object({
+ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
+ projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
+ slug: z
+ .string()
+ .min(1)
+ .max(60)
+ .trim()
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .optional()
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
+ permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
+ }),
+ response: {
+ 200: z.object({
+ privilege: IdentityProjectAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const privilege = await server.services.identityProjectAdditionalPrivilege.create({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body,
+ slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
+ isTemporary: false,
+ permissions: JSON.stringify(packRules(req.body.permissions))
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/temporary",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Create a temporary or a expiring specific privilege for identity.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ body: z.object({
+ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
+ projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
+ slug: z
+ .string()
+ .min(1)
+ .max(60)
+ .trim()
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .optional()
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
+ permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
+ temporaryMode: z
+ .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
+ temporaryRange: z
+ .string()
+ .refine((val) => ms(val) > 0, "Temporary range must be a positive number")
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
+ temporaryAccessStartTime: z
+ .string()
+ .datetime()
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
+ }),
+ response: {
+ 200: z.object({
+ privilege: IdentityProjectAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const privilege = await server.services.identityProjectAdditionalPrivilege.create({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body,
+ slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
+ isTemporary: true,
+ permissions: JSON.stringify(packRules(req.body.permissions))
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Update a specific privilege of an identity.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ body: z.object({
+ // disallow empty string
+ privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.slug),
+ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.identityId),
+ projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
+ privilegeDetails: z
+ .object({
+ slug: z
+ .string()
+ .min(1)
+ .max(60)
+ .trim()
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
+ permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
+ 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")
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
+ temporaryAccessStartTime: z
+ .string()
+ .datetime()
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
+ })
+ .partial()
+ }),
+ response: {
+ 200: z.object({
+ privilege: IdentityProjectAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const updatedInfo = req.body.privilegeDetails;
+ const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ slug: req.body.privilegeSlug,
+ identityId: req.body.identityId,
+ projectSlug: req.body.projectSlug,
+ data: {
+ ...updatedInfo,
+ permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined
+ }
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Delete a specific privilege of an identity.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ body: z.object({
+ privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.slug),
+ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.identityId),
+ projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.projectSlug)
+ }),
+ response: {
+ 200: z.object({
+ privilege: IdentityProjectAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const privilege = await server.services.identityProjectAdditionalPrivilege.deleteBySlug({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ slug: req.body.privilegeSlug,
+ identityId: req.body.identityId,
+ projectSlug: req.body.projectSlug
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:privilegeSlug",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ description: "Retrieve details of a specific privilege by privilege slug.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.slug)
+ }),
+ querystring: z.object({
+ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.identityId),
+ projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.projectSlug)
+ }),
+ response: {
+ 200: z.object({
+ privilege: IdentityProjectAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsBySlug({
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ slug: req.params.privilegeSlug,
+ ...req.query
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ description: "List of a specific privilege of an identity in a project.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ querystring: z.object({
+ identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId),
+ projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug),
+ unpacked: z
+ .enum(["false", "true"])
+ .transform((el) => el === "true")
+ .default("true")
+ .describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.unpacked)
+ }),
+ response: {
+ 200: z.object({
+ privileges: IdentityProjectAdditionalPrivilegeSchema.array()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const privileges = await server.services.identityProjectAdditionalPrivilege.listIdentityProjectPrivileges({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ ...req.query
+ });
+ if (req.query.unpacked) {
+ return {
+ privileges: privileges.map(({ permissions, ...el }) => ({
+ ...el,
+ permissions: unpackRules(permissions as PackRule>>[])
+ }))
+ };
+ }
+ return { privileges };
+ }
+ });
+};
diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts
index 7d1492f84..6860098fd 100644
--- a/backend/src/ee/routes/v1/index.ts
+++ b/backend/src/ee/routes/v1/index.ts
@@ -1,3 +1,7 @@
+import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
+import { registerDynamicSecretRouter } from "./dynamic-secret-router";
+import { registerGroupRouter } from "./group-router";
+import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOrgRoleRouter } from "./org-role-router";
@@ -13,6 +17,7 @@ import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSecretVersionRouter } from "./secret-version-router";
import { registerSnapshotRouter } from "./snapshot-router";
import { registerTrustedIpRouter } from "./trusted-ip-router";
+import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
@@ -34,10 +39,27 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerSecretRotationProviderRouter, {
prefix: "/secret-rotation-providers"
});
+
+ await server.register(
+ async (dynamicSecretRouter) => {
+ await dynamicSecretRouter.register(registerDynamicSecretRouter);
+ await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
+ },
+ { prefix: "/dynamic-secrets" }
+ );
+
await server.register(registerSamlRouter, { prefix: "/sso" });
await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
+ await server.register(registerGroupRouter, { prefix: "/groups" });
+ await server.register(
+ async (privilegeRouter) => {
+ await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
+ await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" });
+ },
+ { prefix: "/additional-privilege" }
+ );
};
diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts
index de472ff29..c35d275ae 100644
--- a/backend/src/ee/routes/v1/ldap-router.ts
+++ b/backend/src/ee/routes/v1/ldap-router.ts
@@ -17,6 +17,7 @@ import { z } from "zod";
import { LdapConfigsSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
+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";
@@ -97,8 +98,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/config",
method: "GET",
+ url: "/config",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
@@ -122,6 +126,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.query.organizationId,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return ldap;
@@ -129,8 +134,11 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/config",
method: "POST",
+ url: "/config",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
@@ -151,6 +159,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.body.organizationId,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
@@ -162,6 +171,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/config",
method: "PATCH",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z
@@ -184,6 +196,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.body.organizationId,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts
index 41cd11f7d..fbf1af43b 100644
--- a/backend/src/ee/routes/v1/license-router.ts
+++ b/backend/src/ee/routes/v1/license-router.ts
@@ -3,13 +3,17 @@
// TODO(akhilmhdh): Fix this when licence service gets it type
import { z } from "zod";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerLicenseRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:organizationId/plans/table",
method: "GET",
+ url: "/:organizationId/plans/table",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
querystring: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }),
params: z.object({ organizationId: z.string().trim() }),
@@ -24,6 +28,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
+ actorAuthMethod: req.permission.authMethod,
billingCycle: req.query.billingCycle
});
return data;
@@ -31,8 +36,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/plan",
method: "GET",
+ url: "/:organizationId/plan",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -45,6 +53,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return { plan };
@@ -52,8 +61,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/plans",
method: "GET",
+ url: "/:organizationId/plans",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
querystring: z.object({ workspaceId: z.string().trim().optional() }),
@@ -66,6 +78,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgPlan({
actorId: req.permission.id,
actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return data;
@@ -73,8 +87,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/session/trial",
method: "POST",
+ url: "/:organizationId/session/trial",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ success_url: z.string().trim() }),
@@ -89,6 +106,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
+ actorAuthMethod: req.permission.authMethod,
success_url: req.body.success_url
});
return data;
@@ -98,6 +116,9 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:organizationId/customer-portal-session",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -110,6 +131,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return data;
@@ -117,8 +139,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/plan/billing",
method: "GET",
+ url: "/:organizationId/plan/billing",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -131,6 +156,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return data;
@@ -138,8 +164,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/plan/table",
method: "GET",
+ url: "/:organizationId/plan/table",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -152,6 +181,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return data;
@@ -159,8 +189,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details",
method: "GET",
+ url: "/:organizationId/billing-details",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -173,6 +206,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return data;
@@ -180,8 +214,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details",
method: "PATCH",
+ url: "/:organizationId/billing-details",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
@@ -198,6 +235,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
name: req.body.name,
email: req.body.email
@@ -207,8 +245,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details/payment-methods",
method: "GET",
+ url: "/:organizationId/billing-details/payment-methods",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -221,6 +262,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return data;
@@ -228,8 +270,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details/payment-methods",
method: "POST",
+ url: "/:organizationId/billing-details/payment-methods",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
@@ -246,6 +291,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
success_url: req.body.success_url,
cancel_url: req.body.cancel_url
@@ -255,8 +301,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details/payment-methods/:pmtMethodId",
method: "DELETE",
+ url: "/:organizationId/billing-details/payment-methods/:pmtMethodId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim(),
@@ -271,6 +320,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.delOrgPmtMethods({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
pmtMethodId: req.params.pmtMethodId
@@ -280,8 +330,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details/tax-ids",
method: "GET",
+ url: "/:organizationId/billing-details/tax-ids",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -295,6 +348,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.getOrgTaxIds({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId
});
@@ -303,8 +357,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details/tax-ids",
method: "POST",
+ url: "/:organizationId/billing-details/tax-ids",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -322,6 +379,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.addOrgTaxId({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
type: req.body.type,
@@ -332,8 +390,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/billing-details/tax-ids/:taxId",
method: "DELETE",
+ url: "/:organizationId/billing-details/tax-ids/:taxId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim(),
@@ -348,6 +409,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const data = await server.services.license.delOrgTaxId({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
taxId: req.params.taxId
@@ -357,8 +419,11 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:organizationId/invoices",
method: "GET",
+ url: "/:organizationId/invoices",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -373,15 +438,19 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
- orgId: req.params.organizationId
+ orgId: req.params.organizationId,
+ actorAuthMethod: req.permission.authMethod
});
return data;
}
});
server.route({
- url: "/:organizationId/licenses",
method: "GET",
+ url: "/:organizationId/licenses",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -396,6 +465,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
return data;
diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts
index 1e40d2d80..380f61e23 100644
--- a/backend/src/ee/routes/v1/org-role-router.ts
+++ b/backend/src/ee/routes/v1/org-role-router.ts
@@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
+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";
@@ -9,6 +10,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:organizationId/roles",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -19,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
.min(1)
.trim()
.refine(
- (val) => Object.keys(OrgMembershipRole).includes(val),
+ (val) => !Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
@@ -41,6 +45,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.id,
req.params.organizationId,
req.body,
+ req.permission.authMethod,
req.permission.orgId
);
return { role };
@@ -50,6 +55,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "PATCH",
url: "/:organizationId/roles/:roleId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim(),
@@ -84,6 +92,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.params.organizationId,
req.params.roleId,
req.body,
+ req.permission.authMethod,
req.permission.orgId
);
return { role };
@@ -93,6 +102,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:organizationId/roles/:roleId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim(),
@@ -110,6 +122,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.id,
req.params.organizationId,
req.params.roleId,
+ req.permission.authMethod,
req.permission.orgId
);
return { role };
@@ -119,6 +132,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/roles",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -138,6 +154,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
const roles = await server.services.orgRole.listRoles(
req.permission.id,
req.params.organizationId,
+ req.permission.authMethod,
req.permission.orgId
);
return { data: { roles } };
@@ -147,6 +164,9 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/permissions",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -163,6 +183,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
const { permissions, membership } = await server.services.orgRole.getUserPermission(
req.permission.id,
req.params.organizationId,
+ req.permission.authMethod,
req.permission.orgId
);
return { permissions, membership };
diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts
index f6fd53e5e..9ff1e400f 100644
--- a/backend/src/ee/routes/v1/project-role-router.ts
+++ b/backend/src/ee/routes/v1/project-role-router.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
+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";
@@ -8,6 +9,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/roles",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -31,6 +35,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.id,
req.params.projectId,
req.body,
+ req.permission.authMethod,
req.permission.orgId
);
return { role };
@@ -40,6 +45,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "PATCH",
url: "/:projectId/roles/:roleId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim(),
@@ -65,6 +73,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.params.projectId,
req.params.roleId,
req.body,
+ req.permission.authMethod,
req.permission.orgId
);
return { role };
@@ -74,6 +83,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:projectId/roles/:roleId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim(),
@@ -92,6 +104,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.id,
req.params.projectId,
req.params.roleId,
+ req.permission.authMethod,
req.permission.orgId
);
return { role };
@@ -101,6 +114,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:projectId/roles",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -121,6 +137,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
req.permission.type,
req.permission.id,
req.params.projectId,
+ req.permission.authMethod,
req.permission.orgId
);
return { data: { roles } };
@@ -130,6 +147,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:projectId/permissions",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -148,8 +168,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
const { permissions, membership } = await server.services.projectRole.getUserPermission(
req.permission.id,
req.params.projectId,
+ req.permission.authMethod,
req.permission.orgId
);
+
return { data: { permissions, membership } };
}
});
diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts
index cfcecb8f0..9795aaf86 100644
--- a/backend/src/ee/routes/v1/project-router.ts
+++ b/backend/src/ee/routes/v1/project-router.ts
@@ -2,7 +2,9 @@ import { z } from "zod";
import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
-import { removeTrailingSlash } from "@app/lib/fn";
+import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
+import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
+import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -10,22 +12,24 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/secret-snapshots",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Return project secret snapshots ids",
security: [
{
- apiKeyAuth: [],
bearerAuth: []
}
],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.workspaceId)
}),
querystring: z.object({
- environment: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash),
- offset: z.coerce.number().default(0),
- limit: z.coerce.number().default(20)
+ environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(PROJECTS.GET_SNAPSHOTS.path),
+ offset: z.coerce.number().default(0).describe(PROJECTS.GET_SNAPSHOTS.offset),
+ limit: z.coerce.number().default(20).describe(PROJECTS.GET_SNAPSHOTS.limit)
}),
response: {
200: z.object({
@@ -37,6 +41,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const secretSnapshots = await server.services.snapshot.listSnapshots({
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
@@ -49,6 +54,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/secret-snapshots/count",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -68,6 +76,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const count = await server.services.snapshot.projectSecretSnapshotCount({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
environment: req.query.environment,
@@ -80,25 +89,27 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/audit-logs",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Return audit logs",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId)
}),
querystring: z.object({
- eventType: z.nativeEnum(EventType).optional(),
- userAgentType: z.nativeEnum(UserAgentType).optional(),
- startDate: z.string().datetime().optional(),
- endDate: z.string().datetime().optional(),
- offset: z.coerce.number().default(0),
- limit: z.coerce.number().default(20),
- actor: z.string().optional()
+ eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
+ userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
+ startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
+ endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
+ offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
+ limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
+ actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
}),
response: {
200: z.object({
@@ -129,8 +140,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const auditLogs = await server.services.auditLog.listProjectAuditLogs({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
...req.query,
+ startDate: req.query.endDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type
});
@@ -141,6 +154,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:workspaceId/audit-logs/filters/actors",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts
index fe387143e..6cae30f7a 100644
--- a/backend/src/ee/routes/v1/saml-router.ts
+++ b/backend/src/ee/routes/v1/saml-router.ts
@@ -17,6 +17,7 @@ import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
+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";
@@ -203,8 +204,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/config",
method: "GET",
+ url: "/config",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
@@ -231,6 +235,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.query.organizationId,
type: "org"
});
@@ -239,8 +244,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/config",
method: "POST",
+ url: "/config",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
@@ -259,6 +267,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
const saml = await server.services.saml.createSamlCfg({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId,
...req.body
@@ -268,8 +277,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/config",
method: "PATCH",
+ url: "/config",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z
@@ -290,6 +302,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
const saml = await server.services.saml.updateSamlCfg({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId,
...req.body
diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts
index 2a3772cd6..80ece7e85 100644
--- a/backend/src/ee/routes/v1/scim-router.ts
+++ b/backend/src/ee/routes/v1/scim-router.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { ScimTokensSchema } from "@app/db/schemas";
+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";
@@ -20,6 +21,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/scim-tokens",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
@@ -39,6 +43,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId,
+ actorAuthMethod: req.permission.authMethod,
description: req.body.description,
ttlDays: req.body.ttlDays
});
@@ -50,6 +55,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/scim-tokens",
method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
@@ -65,6 +73,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const scimTokens = await server.services.scim.listScimTokens({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.query.organizationId
});
@@ -76,6 +85,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/scim-tokens/:scimTokenId",
method: "DELETE",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -92,6 +104,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
scimTokenId: req.params.scimTokenId,
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
@@ -143,7 +156,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
offset: req.query.startIndex,
limit: req.query.count,
filter: req.query.filter,
- orgId: req.permission.orgId as string
+ orgId: req.permission.orgId
});
return users;
}
@@ -181,7 +194,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const user = await req.server.services.scim.getScimUser({
userId: req.params.userId,
- orgId: req.permission.orgId as string
+ orgId: req.permission.orgId
});
return user;
}
@@ -193,7 +206,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
schemas: z.array(z.string()),
- userName: z.string().trim().email(),
+ userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
@@ -214,7 +227,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
- userName: z.string().trim().email(),
+ userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
@@ -240,7 +253,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
email: primaryEmail,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
- orgId: req.permission.orgId as string
+ orgId: req.permission.orgId
});
return user;
@@ -249,38 +262,257 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/Users/:userId",
- method: "PATCH",
+ method: "DELETE",
schema: {
params: z.object({
userId: z.string().trim()
}),
- body: z.object({
- schemas: z.array(z.string()),
- Operations: z.array(
- z.object({
- op: z.string().trim(),
- path: z.string().trim().optional(),
- value: z.union([
- z.object({
- active: z.boolean()
- }),
- z.string().trim()
- ])
- })
- )
- }),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
- const user = await req.server.services.scim.updateScimUser({
+ const user = await req.server.services.scim.deleteScimUser({
userId: req.params.userId,
- orgId: req.permission.orgId as string,
+ orgId: req.permission.orgId
+ });
+
+ return user;
+ }
+ });
+
+ server.route({
+ url: "/Groups",
+ method: "POST",
+ schema: {
+ body: z.object({
+ schemas: z.array(z.string()),
+ displayName: z.string().trim(),
+ members: z.array(z.any()).length(0).optional() // okta-specific
+ }),
+ response: {
+ 200: z.object({
+ schemas: z.array(z.string()),
+ id: z.string().trim(),
+ displayName: z.string().trim(),
+ members: z.array(z.any()).length(0),
+ meta: z.object({
+ resourceType: z.string().trim()
+ })
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
+ handler: async (req) => {
+ const group = await req.server.services.scim.createScimGroup({
+ displayName: req.body.displayName,
+ orgId: req.permission.orgId
+ });
+
+ return group;
+ }
+ });
+
+ server.route({
+ url: "/Groups",
+ method: "GET",
+ schema: {
+ querystring: z.object({
+ startIndex: z.coerce.number().default(1),
+ count: z.coerce.number().default(20),
+ filter: z.string().trim().optional()
+ }),
+ response: {
+ 200: z.object({
+ Resources: z.array(
+ z.object({
+ schemas: z.array(z.string()),
+ id: z.string().trim(),
+ displayName: z.string().trim(),
+ members: z.array(z.any()).length(0),
+ meta: z.object({
+ resourceType: z.string().trim()
+ })
+ })
+ ),
+ itemsPerPage: z.number(),
+ schemas: z.array(z.string()),
+ startIndex: z.number(),
+ totalResults: z.number()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
+ handler: async (req) => {
+ const groups = await req.server.services.scim.listScimGroups({
+ orgId: req.permission.orgId,
+ offset: req.query.startIndex,
+ limit: req.query.count
+ });
+
+ return groups;
+ }
+ });
+
+ server.route({
+ url: "/Groups/:groupId",
+ method: "GET",
+ schema: {
+ params: z.object({
+ groupId: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ schemas: z.array(z.string()),
+ id: z.string().trim(),
+ displayName: z.string().trim(),
+ members: z.array(
+ z.object({
+ value: z.string(),
+ display: z.string()
+ })
+ ),
+ meta: z.object({
+ resourceType: z.string().trim()
+ })
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
+ handler: async (req) => {
+ const group = await req.server.services.scim.getScimGroup({
+ groupId: req.params.groupId,
+ orgId: req.permission.orgId
+ });
+ return group;
+ }
+ });
+
+ server.route({
+ url: "/Groups/:groupId",
+ method: "PUT",
+ schema: {
+ params: z.object({
+ groupId: z.string().trim()
+ }),
+ body: z.object({
+ schemas: z.array(z.string()),
+ id: z.string().trim(),
+ displayName: z.string().trim(),
+ members: z.array(z.any()).length(0)
+ }),
+ response: {
+ 200: z.object({
+ schemas: z.array(z.string()),
+ id: z.string().trim(),
+ displayName: z.string().trim(),
+ members: z.array(
+ z.object({
+ value: z.string(),
+ display: z.string()
+ })
+ ),
+ meta: z.object({
+ resourceType: z.string().trim()
+ })
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
+ handler: async (req) => {
+ const group = await req.server.services.scim.updateScimGroupNamePut({
+ groupId: req.params.groupId,
+ orgId: req.permission.orgId,
+ displayName: req.body.displayName
+ });
+
+ return group;
+ }
+ });
+
+ server.route({
+ url: "/Groups/:groupId",
+ method: "PATCH",
+ schema: {
+ params: z.object({
+ groupId: z.string().trim()
+ }),
+ body: z.object({
+ schemas: z.array(z.string()),
+ Operations: z.array(
+ z.union([
+ z.object({
+ op: z.literal("replace"),
+ value: z.object({
+ id: z.string().trim(),
+ displayName: z.string().trim()
+ })
+ }),
+ z.object({
+ op: z.literal("remove"),
+ path: z.string().trim()
+ }),
+ z.object({
+ op: z.literal("add"),
+ value: z.object({
+ value: z.string().trim(),
+ display: z.string().trim().optional()
+ })
+ })
+ ])
+ )
+ }),
+ response: {
+ 200: z.object({
+ schemas: z.array(z.string()),
+ id: z.string().trim(),
+ displayName: z.string().trim(),
+ members: z.array(
+ z.object({
+ value: z.string(),
+ display: z.string()
+ })
+ ),
+ meta: z.object({
+ resourceType: z.string().trim()
+ })
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
+ handler: async (req) => {
+ // console.log("PATCH /Groups/:groupId req.body: ", req.body);
+ // console.log("PATCH /Groups/:groupId req.body: ", req.body.Operations[0]);
+ const group = await req.server.services.scim.updateScimGroupNamePatch({
+ groupId: req.params.groupId,
+ orgId: req.permission.orgId,
operations: req.body.Operations
});
- return user;
+
+ return group;
+ }
+ });
+
+ server.route({
+ url: "/Groups/:groupId",
+ method: "DELETE",
+ schema: {
+ params: z.object({
+ groupId: z.string().trim()
+ }),
+ response: {
+ 200: z.object({})
+ }
+ },
+ onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
+ handler: async (req) => {
+ const group = await req.server.services.scim.deleteScimGroup({
+ groupId: req.params.groupId,
+ orgId: req.permission.orgId
+ });
+
+ return group;
}
});
@@ -327,7 +559,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
userId: req.params.userId,
- orgId: req.permission.orgId as string,
+ orgId: req.permission.orgId,
active: req.body.active
});
return user;
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 8fce232a7..f6a955625 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -9,6 +10,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z
.object({
@@ -34,6 +38,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...req.body,
@@ -46,6 +51,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/:sapId",
method: "PATCH",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
sapId: z.string()
@@ -72,6 +80,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
secretPolicyId: req.params.sapId
@@ -83,6 +92,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/:sapId",
method: "DELETE",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
sapId: z.string()
@@ -98,6 +110,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPolicyId: req.params.sapId
});
@@ -108,6 +121,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/",
method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
querystring: z.object({
workspaceId: z.string().trim()
@@ -123,6 +139,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId
});
@@ -133,6 +150,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({
url: "/board",
method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
@@ -150,6 +170,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId,
...req.query
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 97eb89109..2a9cc405d 100644
--- a/backend/src/ee/routes/v1/secret-approval-request-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts
@@ -10,13 +10,17 @@ import {
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-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";
export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/",
method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
@@ -52,6 +56,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
@@ -61,8 +66,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
});
server.route({
- url: "/count",
method: "GET",
+ url: "/count",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
querystring: z.object({
workspaceId: z.string().trim()
@@ -81,6 +89,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approvals = await server.services.secretApprovalRequest.requestCount({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId
});
@@ -91,6 +100,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
server.route({
url: "/:id/merge",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
id: z.string()
@@ -106,6 +118,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
approvalId: req.params.id
});
@@ -114,8 +127,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
});
server.route({
- url: "/:id/review",
method: "POST",
+ url: "/:id/review",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
id: z.string()
@@ -134,6 +150,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const review = await server.services.secretApprovalRequest.reviewApproval({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
approvalId: req.params.id,
status: req.body.status
@@ -143,8 +160,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
});
server.route({
- url: "/:id/status",
method: "POST",
+ url: "/:id/status",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
id: z.string()
@@ -163,6 +183,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approval = await server.services.secretApprovalRequest.updateApprovalStatus({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
approvalId: req.params.id,
status: req.body.status
@@ -198,8 +219,11 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.array()
.optional();
server.route({
- url: "/:id",
method: "GET",
+ url: "/:id",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
id: z.string()
@@ -271,6 +295,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
diff --git a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts
index e7201b73f..58419d3b7 100644
--- a/backend/src/ee/routes/v1/secret-rotation-provider-router.ts
+++ b/backend/src/ee/routes/v1/secret-rotation-provider-router.ts
@@ -1,12 +1,16 @@
import { z } from "zod";
+import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSecretRotationProviderRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:workspaceId",
method: "GET",
+ url: "/:workspaceId",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -30,6 +34,7 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
const providers = await server.services.secretRotation.getProviderTemplates({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
diff --git a/backend/src/ee/routes/v1/secret-rotation-router.ts b/backend/src/ee/routes/v1/secret-rotation-router.ts
index 8d2e90ac0..d951eb744 100644
--- a/backend/src/ee/routes/v1/secret-rotation-router.ts
+++ b/backend/src/ee/routes/v1/secret-rotation-router.ts
@@ -2,13 +2,17 @@ import { z } from "zod";
import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas";
import { removeTrailingSlash } from "@app/lib/fn";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSecretRotationRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/",
method: "POST",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
workspaceId: z.string().trim(),
@@ -39,6 +43,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
handler: async (req) => {
const secretRotation = await server.services.secretRotation.createRotation({
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
...req.body,
@@ -51,6 +56,9 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
server.route({
url: "/restart",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
id: z.string().trim()
@@ -74,6 +82,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
const secretRotation = await server.services.secretRotation.restartById({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
rotationId: req.body.id
});
@@ -84,6 +93,9 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
server.route({
url: "/",
method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
querystring: z.object({
workspaceId: z.string().trim()
@@ -125,6 +137,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
const secretRotations = await server.services.secretRotation.getByProjectId({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.query.workspaceId
});
@@ -133,8 +146,11 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
});
server.route({
- url: "/:id",
method: "DELETE",
+ url: "/:id",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
id: z.string().trim()
@@ -158,6 +174,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
const secretRotation = await server.services.secretRotation.deleteById({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
rotationId: req.params.id
});
diff --git a/backend/src/ee/routes/v1/secret-scanning-router.ts b/backend/src/ee/routes/v1/secret-scanning-router.ts
index 7d2c5f1ee..2604d7232 100644
--- a/backend/src/ee/routes/v1/secret-scanning-router.ts
+++ b/backend/src/ee/routes/v1/secret-scanning-router.ts
@@ -2,13 +2,17 @@ import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-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";
export const registerSecretScanningRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/create-installation-session/organization",
method: "POST",
+ url: "/create-installation-session/organization",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({ organizationId: z.string().trim() }),
response: {
@@ -22,6 +26,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const session = await server.services.secretScanning.createInstallationSession({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.body.organizationId
});
@@ -30,8 +35,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
});
server.route({
- url: "/link-installation",
method: "POST",
+ url: "/link-installation",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
installationId: z.string(),
@@ -46,6 +54,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const { installatedApp } = await server.services.secretScanning.linkInstallationToOrg({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
@@ -54,8 +63,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
});
server.route({
- url: "/installation-status/organization/:organizationId",
method: "GET",
+ url: "/installation-status/organization/:organizationId",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -67,6 +79,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const appInstallationCompleted = await server.services.secretScanning.getOrgInstallationStatus({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId
});
@@ -77,6 +90,9 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
server.route({
url: "/organization/:organizationId/risks",
method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -88,6 +104,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const { risks } = await server.services.secretScanning.getRisksByOrg({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId
});
@@ -96,8 +113,11 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
});
server.route({
- url: "/organization/:organizationId/risks/:riskId/status",
method: "POST",
+ url: "/organization/:organizationId/risks/:riskId/status",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim(), riskId: z.string().trim() }),
body: z.object({ status: z.nativeEnum(SecretScanningRiskStatus) }),
@@ -110,6 +130,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
const { risk } = await server.services.secretScanning.updateRiskStatus({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.organizationId,
riskId: req.params.riskId,
diff --git a/backend/src/ee/routes/v1/secret-version-router.ts b/backend/src/ee/routes/v1/secret-version-router.ts
index 89ee4e011..0604135ba 100644
--- a/backend/src/ee/routes/v1/secret-version-router.ts
+++ b/backend/src/ee/routes/v1/secret-version-router.ts
@@ -1,13 +1,17 @@
import { z } from "zod";
import { SecretVersionsSchema } from "@app/db/schemas";
+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 registerSecretVersionRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:secretId/secret-versions",
method: "GET",
+ url: "/:secretId/secret-versions",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
secretId: z.string()
@@ -27,6 +31,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
const secretVersions = await server.services.secret.getSecretVersions({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
limit: req.query.limit,
offset: req.query.offset,
diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts
index 0b858255f..6767f8383 100644
--- a/backend/src/ee/routes/v1/snapshot-router.ts
+++ b/backend/src/ee/routes/v1/snapshot-router.ts
@@ -1,6 +1,8 @@
import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas";
+import { PROJECTS } 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";
@@ -8,6 +10,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:secretSnapshotId",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
secretSnapshotId: z.string().trim()
@@ -46,6 +51,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
const secretSnapshot = await server.services.snapshot.getSnapshotData({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId
});
@@ -56,16 +62,18 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:secretSnapshotId/rollback",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Roll back project secrets to those captured in a secret snapshot version.",
security: [
{
- apiKeyAuth: [],
bearerAuth: []
}
],
params: z.object({
- secretSnapshotId: z.string().trim()
+ secretSnapshotId: z.string().trim().describe(PROJECTS.ROLLBACK_TO_SNAPSHOT.secretSnapshotId)
}),
response: {
200: z.object({
@@ -78,6 +86,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
const secretSnapshot = await server.services.snapshot.rollbackSnapshot({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId
});
diff --git a/backend/src/ee/routes/v1/trusted-ip-router.ts b/backend/src/ee/routes/v1/trusted-ip-router.ts
index 53bc5b117..b6fc3cc90 100644
--- a/backend/src/ee/routes/v1/trusted-ip-router.ts
+++ b/backend/src/ee/routes/v1/trusted-ip-router.ts
@@ -2,13 +2,17 @@ import { z } from "zod";
import { TrustedIpsSchema } 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";
export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:workspaceId/trusted-ips",
method: "GET",
+ url: "/:workspaceId/trusted-ips",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -22,6 +26,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const trustedIps = await server.services.trustedIp.listIpsByProjectId({
+ actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -32,8 +37,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/trusted-ips",
method: "POST",
+ url: "/:workspaceId/trusted-ips",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -52,6 +60,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { trustedIp, project } = await server.services.trustedIp.addProjectIp({
+ actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -76,8 +85,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/trusted-ips/:trustedIpId",
method: "PATCH",
+ url: "/:workspaceId/trusted-ips/:trustedIpId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim(),
@@ -99,6 +111,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
trustedIpId: req.params.trustedIpId,
...req.body
@@ -121,8 +134,11 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/trusted-ips/:trustedIpId",
method: "DELETE",
+ url: "/:workspaceId/trusted-ips/:trustedIpId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim(),
@@ -140,6 +156,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
trustedIpId: req.params.trustedIpId
});
diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts
new file mode 100644
index 000000000..7225caecf
--- /dev/null
+++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts
@@ -0,0 +1,256 @@
+import slugify from "@sindresorhus/slugify";
+import ms from "ms";
+import { z } from "zod";
+
+import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
+import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
+import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
+import { alphaNumericNanoId } from "@app/lib/nanoid";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ url: "/permanent",
+ method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ body: z.object({
+ projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
+ slug: z
+ .string()
+ .min(1)
+ .max(60)
+ .trim()
+ .refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .optional()
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
+ permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
+ }),
+ response: {
+ 200: z.object({
+ privilege: ProjectUserAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const privilege = await server.services.projectUserAdditionalPrivilege.create({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body,
+ slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
+ isTemporary: false,
+ permissions: JSON.stringify(req.body.permissions)
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "POST",
+ url: "/temporary",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ body: z.object({
+ projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
+ slug: z
+ .string()
+ .min(1)
+ .max(60)
+ .trim()
+ .refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .optional()
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
+ permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
+ temporaryMode: z
+ .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
+ temporaryRange: z
+ .string()
+ .refine((val) => ms(val) > 0, "Temporary range must be a positive number")
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange),
+ temporaryAccessStartTime: z
+ .string()
+ .datetime()
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime)
+ }),
+ response: {
+ 200: z.object({
+ privilege: ProjectUserAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const privilege = await server.services.projectUserAdditionalPrivilege.create({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body,
+ slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
+ isTemporary: true,
+ permissions: JSON.stringify(req.body.permissions)
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/:privilegeId",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ privilegeId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.privilegeId)
+ }),
+ body: z
+ .object({
+ slug: z
+ .string()
+ .max(60)
+ .trim()
+ .refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
+ permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
+ isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
+ temporaryMode: z
+ .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
+ temporaryRange: z
+ .string()
+ .refine((val) => ms(val) > 0, "Temporary range must be a positive number")
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
+ temporaryAccessStartTime: z
+ .string()
+ .datetime()
+ .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime)
+ })
+ .partial(),
+ response: {
+ 200: z.object({
+ privilege: ProjectUserAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const privilege = await server.services.projectUserAdditionalPrivilege.updateById({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body,
+ permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined,
+ privilegeId: req.params.privilegeId
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:privilegeId",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.DELETE.privilegeId)
+ }),
+ response: {
+ 200: z.object({
+ privilege: ProjectUserAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const privilege = await server.services.projectUserAdditionalPrivilege.deleteById({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ privilegeId: req.params.privilegeId
+ });
+ return { privilege };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ querystring: z.object({
+ projectMembershipId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.LIST.projectMembershipId)
+ }),
+ response: {
+ 200: z.object({
+ privileges: ProjectUserAdditionalPrivilegeSchema.array()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const privileges = await server.services.projectUserAdditionalPrivilege.listPrivileges({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ projectMembershipId: req.query.projectMembershipId
+ });
+ return { privileges };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:privilegeId",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGEID.privilegeId)
+ }),
+ response: {
+ 200: z.object({
+ privilege: ProjectUserAdditionalPrivilegeSchema
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const privilege = await server.services.projectUserAdditionalPrivilege.getPrivilegeDetailsById({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ privilegeId: req.params.privilegeId
+ });
+ return { privilege };
+ }
+ });
+};
diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts
index c4d4aabc0..1564c6dcb 100644
--- a/backend/src/ee/services/audit-log/audit-log-service.ts
+++ b/backend/src/ee/services/audit-log/audit-log-service.ts
@@ -31,10 +31,17 @@ export const auditLogServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
projectId,
auditLogActor
}: TListProjectAuditLogDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
const auditLogs = await auditLogDAL.find({
startDate,
diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts
new file mode 100644
index 000000000..810628030
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts
@@ -0,0 +1,80 @@
+import { Knex } from "knex";
+
+import { TDbClient } from "@app/db";
+import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
+import { ormify, selectAllTableCols } from "@app/lib/knex";
+
+export type TDynamicSecretLeaseDALFactory = ReturnType;
+
+export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
+ const orm = ormify(db, TableName.DynamicSecretLease);
+
+ const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
+ try {
+ const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
+ return parseInt(doc || "0", 10);
+ } catch (error) {
+ throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
+ }
+ };
+
+ const findById = async (id: string, tx?: Knex) => {
+ try {
+ const doc = await (tx || db)(TableName.DynamicSecretLease)
+ .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
+ .first()
+ .join(
+ TableName.DynamicSecret,
+ `${TableName.DynamicSecretLease}.dynamicSecretId`,
+ `${TableName.DynamicSecret}.id`
+ )
+ .select(selectAllTableCols(TableName.DynamicSecretLease))
+ .select(
+ db.ref("id").withSchema(TableName.DynamicSecret).as("dynId"),
+ db.ref("name").withSchema(TableName.DynamicSecret).as("dynName"),
+ db.ref("version").withSchema(TableName.DynamicSecret).as("dynVersion"),
+ db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
+ db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
+ db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
+ db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"),
+ db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"),
+ db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"),
+ db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"),
+ db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"),
+ db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
+ db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
+ db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
+ db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
+ db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
+ );
+ if (!doc) return;
+
+ return {
+ ...DynamicSecretLeasesSchema.parse(doc),
+ dynamicSecret: {
+ id: doc.dynId,
+ name: doc.dynName,
+ version: doc.dynVersion,
+ type: doc.dynType,
+ defaultTTL: doc.dynDefaultTTL,
+ maxTTL: doc.dynMaxTTL,
+ inputIV: doc.dynInputIV,
+ inputTag: doc.dynInputTag,
+ inputCiphertext: doc.dynInputCiphertext,
+ algorithm: doc.dynAlgorithm,
+ keyEncoding: doc.dynKeyEncoding,
+ folderId: doc.dynFolderId,
+ status: doc.dynStatus,
+ statusDetails: doc.dynStatusDetails,
+ createdAt: doc.dynCreatedAt,
+ updatedAt: doc.dynUpdatedAt
+ }
+ };
+ } catch (error) {
+ throw new DatabaseError({ error, name: "DynamicSecretLeaseFindById" });
+ }
+ };
+
+ return { ...orm, findById, countLeasesForDynamicSecret };
+};
diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts
new file mode 100644
index 000000000..9bdb1c24e
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts
@@ -0,0 +1,159 @@
+import { SecretKeyEncoding } from "@app/db/schemas";
+import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
+import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
+import { logger } from "@app/lib/logger";
+import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
+
+import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
+import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
+import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
+import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
+
+type TDynamicSecretLeaseQueueServiceFactoryDep = {
+ queueService: TQueueServiceFactory;
+ dynamicSecretLeaseDAL: Pick;
+ dynamicSecretDAL: Pick;
+ dynamicSecretProviders: Record;
+};
+
+export type TDynamicSecretLeaseQueueServiceFactory = ReturnType;
+
+export const dynamicSecretLeaseQueueServiceFactory = ({
+ queueService,
+ dynamicSecretDAL,
+ dynamicSecretProviders,
+ dynamicSecretLeaseDAL
+}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
+ const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
+ await queueService.queue(
+ QueueName.DynamicSecretRevocation,
+ QueueJobs.DynamicSecretPruning,
+ { dynamicSecretCfgId },
+ {
+ jobId: dynamicSecretCfgId,
+ backoff: {
+ type: "exponential",
+ delay: 3000
+ },
+ removeOnFail: {
+ count: 3
+ },
+ removeOnComplete: true
+ }
+ );
+ };
+
+ const setLeaseRevocation = async (leaseId: string, expiry: number) => {
+ await queueService.queue(
+ QueueName.DynamicSecretRevocation,
+ QueueJobs.DynamicSecretRevocation,
+ { leaseId },
+ {
+ jobId: leaseId,
+ backoff: {
+ type: "exponential",
+ delay: 3000
+ },
+ delay: expiry,
+ removeOnFail: {
+ count: 3
+ },
+ removeOnComplete: true
+ }
+ );
+ };
+
+ const unsetLeaseRevocation = async (leaseId: string) => {
+ await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId);
+ };
+
+ queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
+ try {
+ if (job.name === QueueJobs.DynamicSecretRevocation) {
+ const { leaseId } = job.data as { leaseId: string };
+ logger.info("Dynamic secret lease revocation started: ", leaseId, job.id);
+
+ const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
+ if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
+
+ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
+ const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
+ const decryptedStoredInput = JSON.parse(
+ infisicalSymmetricDecrypt({
+ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
+ ciphertext: dynamicSecretCfg.inputCiphertext,
+ tag: dynamicSecretCfg.inputTag,
+ iv: dynamicSecretCfg.inputIV
+ })
+ ) as object;
+
+ await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
+ await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
+ return;
+ }
+
+ if (job.name === QueueJobs.DynamicSecretPruning) {
+ const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
+ logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
+ const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
+ if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
+ if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
+ throw new DisableRotationErrors({ message: "Document not deleted" });
+
+ const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
+ if (dynamicSecretLeases.length) {
+ const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
+ const decryptedStoredInput = JSON.parse(
+ infisicalSymmetricDecrypt({
+ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
+ ciphertext: dynamicSecretCfg.inputCiphertext,
+ tag: dynamicSecretCfg.inputTag,
+ iv: dynamicSecretCfg.inputIV
+ })
+ ) as object;
+
+ await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
+ await Promise.all(
+ dynamicSecretLeases.map(({ externalEntityId }) =>
+ selectedProvider.revoke(decryptedStoredInput, externalEntityId)
+ )
+ );
+ }
+
+ await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
+ }
+ logger.info("Finished dynamic secret job", job.id);
+ } catch (error) {
+ logger.error(error);
+
+ if (job?.name === QueueJobs.DynamicSecretPruning) {
+ const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
+ await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
+ status: DynamicSecretStatus.FailedDeletion,
+ statusDetails: (error as Error)?.message?.slice(0, 255)
+ });
+ }
+
+ if (job?.name === QueueJobs.DynamicSecretRevocation) {
+ const { leaseId } = job.data as { leaseId: string };
+ await dynamicSecretLeaseDAL.updateById(leaseId, {
+ status: DynamicSecretStatus.FailedDeletion,
+ statusDetails: (error as Error)?.message?.slice(0, 255)
+ });
+ }
+ if (error instanceof DisableRotationErrors) {
+ if (job.id) {
+ await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id);
+ }
+ }
+ // propogate to next part
+ throw error;
+ }
+ });
+
+ return {
+ pruneDynamicSecret,
+ setLeaseRevocation,
+ unsetLeaseRevocation
+ };
+};
diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts
new file mode 100644
index 000000000..1e5487d22
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts
@@ -0,0 +1,343 @@
+import { ForbiddenError, subject } from "@casl/ability";
+import ms from "ms";
+
+import { SecretKeyEncoding } from "@app/db/schemas";
+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 { getConfig } from "@app/lib/config/env";
+import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
+import { BadRequestError } from "@app/lib/errors";
+import { logger } from "@app/lib/logger";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
+import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
+
+import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
+import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
+import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
+import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
+import {
+ DynamicSecretLeaseStatus,
+ TCreateDynamicSecretLeaseDTO,
+ TDeleteDynamicSecretLeaseDTO,
+ TDetailsDynamicSecretLeaseDTO,
+ TListDynamicSecretLeasesDTO,
+ TRenewDynamicSecretLeaseDTO
+} from "./dynamic-secret-lease-types";
+
+type TDynamicSecretLeaseServiceFactoryDep = {
+ dynamicSecretLeaseDAL: TDynamicSecretLeaseDALFactory;
+ dynamicSecretDAL: Pick;
+ dynamicSecretProviders: Record;
+ dynamicSecretQueueService: TDynamicSecretLeaseQueueServiceFactory;
+ licenseService: Pick;
+ folderDAL: Pick;
+ permissionService: Pick;
+ projectDAL: Pick;
+};
+
+export type TDynamicSecretLeaseServiceFactory = ReturnType;
+
+export const dynamicSecretLeaseServiceFactory = ({
+ dynamicSecretLeaseDAL,
+ dynamicSecretProviders,
+ dynamicSecretDAL,
+ folderDAL,
+ permissionService,
+ dynamicSecretQueueService,
+ projectDAL,
+ licenseService
+}: TDynamicSecretLeaseServiceFactoryDep) => {
+ const create = async ({
+ environmentSlug,
+ path,
+ name,
+ projectSlug,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ ttl
+ }: TCreateDynamicSecretLeaseDTO) => {
+ const appCfg = getConfig();
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const plan = await licenseService.getPlan(actorOrgId);
+ if (!plan?.dynamicSecret) {
+ throw new BadRequestError({
+ message: "Failed to create lease due to plan restriction. Upgrade plan to create dynamic secret."
+ });
+ }
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
+ if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
+
+ const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
+ if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
+ throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
+
+ const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
+ const decryptedStoredInput = JSON.parse(
+ infisicalSymmetricDecrypt({
+ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
+ ciphertext: dynamicSecretCfg.inputCiphertext,
+ tag: dynamicSecretCfg.inputTag,
+ iv: dynamicSecretCfg.inputIV
+ })
+ ) as object;
+
+ const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
+ const { maxTTL } = dynamicSecretCfg;
+ const expireAt = new Date(new Date().getTime() + ms(selectedTTL));
+ if (maxTTL) {
+ const maxExpiryDate = new Date(new Date().getTime() + ms(maxTTL));
+ if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" });
+ }
+
+ const { entityId, data } = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
+ const dynamicSecretLease = await dynamicSecretLeaseDAL.create({
+ expireAt,
+ version: 1,
+ dynamicSecretId: dynamicSecretCfg.id,
+ externalEntityId: entityId
+ });
+ await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
+ return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
+ };
+
+ const renewLease = async ({
+ ttl,
+ actorAuthMethod,
+ actorOrgId,
+ actorId,
+ actor,
+ projectSlug,
+ path,
+ environmentSlug,
+ leaseId
+ }: TRenewDynamicSecretLeaseDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Edit,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const plan = await licenseService.getPlan(actorOrgId);
+ if (!plan?.dynamicSecret) {
+ throw new BadRequestError({
+ message: "Failed to renew lease due to plan restriction. Upgrade plan to create dynamic secret."
+ });
+ }
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
+ if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
+
+ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
+ const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
+ const decryptedStoredInput = JSON.parse(
+ infisicalSymmetricDecrypt({
+ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
+ ciphertext: dynamicSecretCfg.inputCiphertext,
+ tag: dynamicSecretCfg.inputTag,
+ iv: dynamicSecretCfg.inputIV
+ })
+ ) as object;
+
+ const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
+ const { maxTTL } = dynamicSecretCfg;
+ const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
+ if (maxTTL) {
+ const maxExpiryDate = new Date(dynamicSecretLease.createdAt.getTime() + ms(maxTTL));
+ if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max ttl" });
+ }
+
+ const { entityId } = await selectedProvider.renew(
+ decryptedStoredInput,
+ dynamicSecretLease.externalEntityId,
+ expireAt.getTime()
+ );
+
+ await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
+ await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
+ const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
+ expireAt,
+ externalEntityId: entityId
+ });
+ return updatedDynamicSecretLease;
+ };
+
+ const revokeLease = async ({
+ leaseId,
+ environmentSlug,
+ path,
+ projectSlug,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ isForced
+ }: TDeleteDynamicSecretLeaseDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Delete,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
+ if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
+
+ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
+ const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
+ const decryptedStoredInput = JSON.parse(
+ infisicalSymmetricDecrypt({
+ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
+ ciphertext: dynamicSecretCfg.inputCiphertext,
+ tag: dynamicSecretCfg.inputTag,
+ iv: dynamicSecretCfg.inputIV
+ })
+ ) as object;
+
+ const revokeResponse = await selectedProvider
+ .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
+ .catch(async (err) => {
+ // only propogate this error if forced is false
+ if (!isForced) return { error: err as Error };
+ });
+
+ if ((revokeResponse as { error?: Error })?.error) {
+ const { error } = revokeResponse as { error?: Error };
+ logger.error(error?.message, "Failed to revoke lease");
+ const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
+ status: DynamicSecretLeaseStatus.FailedDeletion,
+ statusDetails: error?.message?.slice(0, 255)
+ });
+ return deletedDynamicSecretLease;
+ }
+
+ await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
+ const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
+ return deletedDynamicSecretLease;
+ };
+
+ const listLeases = async ({
+ path,
+ name,
+ actor,
+ actorId,
+ projectSlug,
+ actorOrgId,
+ environmentSlug,
+ actorAuthMethod
+ }: TListDynamicSecretLeasesDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
+ if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
+
+ const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
+ return dynamicSecretLeases;
+ };
+
+ const getLeaseDetails = async ({
+ projectSlug,
+ actorOrgId,
+ path,
+ environmentSlug,
+ actor,
+ actorId,
+ leaseId,
+ actorAuthMethod
+ }: TDetailsDynamicSecretLeaseDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
+ if (!dynamicSecretLease) throw new BadRequestError({ message: "Dynamic secret lease not found" });
+
+ return dynamicSecretLease;
+ };
+
+ return {
+ create,
+ listLeases,
+ revokeLease,
+ renewLease,
+ getLeaseDetails
+ };
+};
diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-types.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-types.ts
new file mode 100644
index 000000000..bf182b349
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-types.ts
@@ -0,0 +1,43 @@
+import { TProjectPermission } from "@app/lib/types";
+
+export enum DynamicSecretLeaseStatus {
+ FailedDeletion = "Failed to delete"
+}
+
+export type TCreateDynamicSecretLeaseDTO = {
+ name: string;
+ path: string;
+ environmentSlug: string;
+ ttl?: string;
+ projectSlug: string;
+} & Omit;
+
+export type TDetailsDynamicSecretLeaseDTO = {
+ leaseId: string;
+ path: string;
+ environmentSlug: string;
+ projectSlug: string;
+} & Omit;
+
+export type TListDynamicSecretLeasesDTO = {
+ name: string;
+ path: string;
+ environmentSlug: string;
+ projectSlug: string;
+} & Omit;
+
+export type TDeleteDynamicSecretLeaseDTO = {
+ leaseId: string;
+ path: string;
+ environmentSlug: string;
+ projectSlug: string;
+ isForced?: boolean;
+} & Omit;
+
+export type TRenewDynamicSecretLeaseDTO = {
+ leaseId: string;
+ path: string;
+ environmentSlug: string;
+ ttl?: string;
+ projectSlug: string;
+} & Omit;
diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts
new file mode 100644
index 000000000..0cc4aca2f
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret/dynamic-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 TDynamicSecretDALFactory = ReturnType;
+
+export const dynamicSecretDALFactory = (db: TDbClient) => {
+ const orm = ormify(db, TableName.DynamicSecret);
+ return orm;
+};
diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts
new file mode 100644
index 000000000..1aef3cc86
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts
@@ -0,0 +1,341 @@
+import { ForbiddenError, subject } from "@casl/ability";
+
+import { SecretKeyEncoding } from "@app/db/schemas";
+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 { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
+import { BadRequestError } from "@app/lib/errors";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
+import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
+
+import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
+import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
+import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
+import {
+ DynamicSecretStatus,
+ TCreateDynamicSecretDTO,
+ TDeleteDynamicSecretDTO,
+ TDetailsDynamicSecretDTO,
+ TListDynamicSecretsDTO,
+ TUpdateDynamicSecretDTO
+} from "./dynamic-secret-types";
+import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
+
+type TDynamicSecretServiceFactoryDep = {
+ dynamicSecretDAL: TDynamicSecretDALFactory;
+ dynamicSecretLeaseDAL: Pick;
+ dynamicSecretProviders: Record;
+ dynamicSecretQueueService: Pick<
+ TDynamicSecretLeaseQueueServiceFactory,
+ "pruneDynamicSecret" | "unsetLeaseRevocation"
+ >;
+ licenseService: Pick;
+ folderDAL: Pick;
+ projectDAL: Pick;
+ permissionService: Pick;
+};
+
+export type TDynamicSecretServiceFactory = ReturnType;
+
+export const dynamicSecretServiceFactory = ({
+ dynamicSecretDAL,
+ dynamicSecretLeaseDAL,
+ licenseService,
+ folderDAL,
+ dynamicSecretProviders,
+ permissionService,
+ dynamicSecretQueueService,
+ projectDAL
+}: TDynamicSecretServiceFactoryDep) => {
+ const create = async ({
+ path,
+ actor,
+ name,
+ actorId,
+ maxTTL,
+ provider,
+ environmentSlug,
+ projectSlug,
+ actorOrgId,
+ defaultTTL,
+ actorAuthMethod
+ }: TCreateDynamicSecretDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Create,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const plan = await licenseService.getPlan(actorOrgId);
+ if (!plan?.dynamicSecret) {
+ throw new BadRequestError({
+ message: "Failed to create dynamic secret due to plan restriction. Upgrade plan to create dynamic secret."
+ });
+ }
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
+ if (existingDynamicSecret)
+ throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
+
+ const selectedProvider = dynamicSecretProviders[provider.type];
+ const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
+
+ const isConnected = await selectedProvider.validateConnection(provider.inputs);
+ if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
+
+ const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
+ const dynamicSecretCfg = await dynamicSecretDAL.create({
+ type: provider.type,
+ version: 1,
+ inputIV: encryptedInput.iv,
+ inputTag: encryptedInput.tag,
+ inputCiphertext: encryptedInput.ciphertext,
+ algorithm: encryptedInput.algorithm,
+ keyEncoding: encryptedInput.encoding,
+ maxTTL,
+ defaultTTL,
+ folderId: folder.id,
+ name
+ });
+ return dynamicSecretCfg;
+ };
+
+ const updateByName = async ({
+ name,
+ maxTTL,
+ defaultTTL,
+ inputs,
+ environmentSlug,
+ projectSlug,
+ path,
+ actor,
+ actorId,
+ newName,
+ actorOrgId,
+ actorAuthMethod
+ }: TUpdateDynamicSecretDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Edit,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const plan = await licenseService.getPlan(actorOrgId);
+ if (!plan?.dynamicSecret) {
+ throw new BadRequestError({
+ message: "Failed to update dynamic secret due to plan restriction. Upgrade plan to create dynamic secret."
+ });
+ }
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
+ if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
+
+ if (newName) {
+ const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
+ if (existingDynamicSecret)
+ throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
+ }
+
+ const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
+ const decryptedStoredInput = JSON.parse(
+ infisicalSymmetricDecrypt({
+ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
+ ciphertext: dynamicSecretCfg.inputCiphertext,
+ tag: dynamicSecretCfg.inputTag,
+ iv: dynamicSecretCfg.inputIV
+ })
+ ) as object;
+ const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
+ const updatedInput = await selectedProvider.validateProviderInputs(newInput);
+
+ const isConnected = await selectedProvider.validateConnection(newInput);
+ if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
+
+ const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
+ const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
+ inputIV: encryptedInput.iv,
+ inputTag: encryptedInput.tag,
+ inputCiphertext: encryptedInput.ciphertext,
+ algorithm: encryptedInput.algorithm,
+ keyEncoding: encryptedInput.encoding,
+ maxTTL,
+ defaultTTL,
+ name: newName ?? name,
+ status: null,
+ statusDetails: null
+ });
+
+ return updatedDynamicCfg;
+ };
+
+ const deleteByName = async ({
+ actorAuthMethod,
+ actorOrgId,
+ actorId,
+ actor,
+ projectSlug,
+ name,
+ path,
+ environmentSlug,
+ isForced
+ }: TDeleteDynamicSecretDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Edit,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
+ if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
+
+ const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
+ // when not forced we check with the external system to first remove the things
+ // we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
+ // this allows user to clean up it from infisical
+ if (isForced) {
+ // clear all queues for lease revocations
+ await Promise.all(leases.map(({ id: leaseId }) => dynamicSecretQueueService.unsetLeaseRevocation(leaseId)));
+
+ const deletedDynamicSecretCfg = await dynamicSecretDAL.deleteById(dynamicSecretCfg.id);
+ return deletedDynamicSecretCfg;
+ }
+ // if leases exist we should flag it as deleting and then remove leases in background
+ // then delete the main one
+ if (leases.length) {
+ const updatedDynamicSecretCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
+ status: DynamicSecretStatus.Deleting
+ });
+ await dynamicSecretQueueService.pruneDynamicSecret(updatedDynamicSecretCfg.id);
+ return updatedDynamicSecretCfg;
+ }
+ // if no leases just delete the config
+ const deletedDynamicSecretCfg = await dynamicSecretDAL.deleteById(dynamicSecretCfg.id);
+ return deletedDynamicSecretCfg;
+ };
+
+ const getDetails = async ({
+ name,
+ projectSlug,
+ path,
+ environmentSlug,
+ actorAuthMethod,
+ actorOrgId,
+ actorId,
+ actor
+ }: TDetailsDynamicSecretDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Edit,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
+ if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
+ const decryptedStoredInput = JSON.parse(
+ infisicalSymmetricDecrypt({
+ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
+ ciphertext: dynamicSecretCfg.inputCiphertext,
+ tag: dynamicSecretCfg.inputTag,
+ iv: dynamicSecretCfg.inputIV
+ })
+ ) as object;
+ const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
+ const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
+ return { ...dynamicSecretCfg, inputs: providerInputs };
+ };
+
+ const list = async ({
+ actorAuthMethod,
+ actorOrgId,
+ actorId,
+ actor,
+ projectSlug,
+ path,
+ environmentSlug
+ }: TListDynamicSecretsDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+
+ const projectId = project.id;
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
+ );
+
+ const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
+ if (!folder) throw new BadRequestError({ message: "Folder not found" });
+
+ const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
+ return dynamicSecretCfg;
+ };
+
+ return {
+ create,
+ updateByName,
+ deleteByName,
+ getDetails,
+ list
+ };
+};
diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts
new file mode 100644
index 000000000..02f2cbb86
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts
@@ -0,0 +1,54 @@
+import { z } from "zod";
+
+import { TProjectPermission } from "@app/lib/types";
+
+import { DynamicSecretProviderSchema } from "./providers/models";
+
+// various status for dynamic secret that happens in background
+export enum DynamicSecretStatus {
+ Deleting = "Revocation in process",
+ FailedDeletion = "Failed to delete"
+}
+
+type TProvider = z.infer;
+export type TCreateDynamicSecretDTO = {
+ provider: TProvider;
+ defaultTTL: string;
+ maxTTL?: string | null;
+ path: string;
+ environmentSlug: string;
+ name: string;
+ projectSlug: string;
+} & Omit;
+
+export type TUpdateDynamicSecretDTO = {
+ name: string;
+ newName?: string;
+ defaultTTL?: string;
+ maxTTL?: string | null;
+ path: string;
+ environmentSlug: string;
+ inputs?: TProvider["inputs"];
+ projectSlug: string;
+} & Omit;
+
+export type TDeleteDynamicSecretDTO = {
+ name: string;
+ path: string;
+ environmentSlug: string;
+ projectSlug: string;
+ isForced?: boolean;
+} & Omit;
+
+export type TDetailsDynamicSecretDTO = {
+ name: string;
+ path: string;
+ environmentSlug: string;
+ projectSlug: string;
+} & Omit;
+
+export type TListDynamicSecretsDTO = {
+ path: string;
+ environmentSlug: string;
+ projectSlug: string;
+} & Omit;
diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts
new file mode 100644
index 000000000..d66e60802
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret/providers/index.ts
@@ -0,0 +1,6 @@
+import { DynamicSecretProviders } from "./models";
+import { SqlDatabaseProvider } from "./sql-database";
+
+export const buildDynamicSecretProviders = () => ({
+ [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
+});
diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts
new file mode 100644
index 000000000..d3510d583
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret/providers/models.ts
@@ -0,0 +1,36 @@
+import { z } from "zod";
+
+export enum SqlProviders {
+ Postgres = "postgres",
+ MySQL = "mysql2",
+ Oracle = "oracledb"
+}
+
+export const DynamicSecretSqlDBSchema = z.object({
+ client: z.nativeEnum(SqlProviders),
+ host: z.string().toLowerCase(),
+ port: z.number(),
+ database: z.string(),
+ username: z.string(),
+ password: z.string(),
+ creationStatement: z.string(),
+ revocationStatement: z.string(),
+ renewStatement: z.string().optional(),
+ ca: z.string().optional()
+});
+
+export enum DynamicSecretProviders {
+ SqlDatabase = "sql-database"
+}
+
+export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
+ z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema })
+]);
+
+export type TDynamicProviderFns = {
+ create: (inputs: unknown, expireAt: number) => Promise<{ entityId: string; data: unknown }>;
+ validateConnection: (inputs: unknown) => Promise;
+ validateProviderInputs: (inputs: object) => Promise;
+ revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
+ renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
+};
diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
new file mode 100644
index 000000000..4c1d5438b
--- /dev/null
+++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts
@@ -0,0 +1,162 @@
+import handlebars from "handlebars";
+import knex from "knex";
+import { customAlphabet } from "nanoid";
+import { z } from "zod";
+
+import { getConfig } from "@app/lib/config/env";
+import { BadRequestError } from "@app/lib/errors";
+import { getDbConnectionHost } from "@app/lib/knex";
+import { alphaNumericNanoId } from "@app/lib/nanoid";
+
+import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
+
+const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
+
+const generatePassword = (provider: SqlProviders) => {
+ // oracle has limit of 48 password length
+ const size = provider === SqlProviders.Oracle ? 30 : 48;
+
+ const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
+ return customAlphabet(charset, 48)(size);
+};
+
+const generateUsername = (provider: SqlProviders) => {
+ // For oracle, the client assumes everything is upper case when not using quotes around the password
+ if (provider === SqlProviders.Oracle) return alphaNumericNanoId(32).toUpperCase();
+
+ return alphaNumericNanoId(32);
+};
+
+export const SqlDatabaseProvider = (): TDynamicProviderFns => {
+ const validateProviderInputs = async (inputs: unknown) => {
+ const appCfg = getConfig();
+ const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
+
+ const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
+ if (
+ // localhost
+ providerInputs.host === "localhost" ||
+ providerInputs.host === "127.0.0.1" ||
+ // database infisical uses
+ dbHost === providerInputs.host ||
+ // internal ips
+ providerInputs.host === "host.docker.internal" ||
+ providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
+ providerInputs.host.match(/^192\.168\.\d+\.\d+/)
+ )
+ throw new BadRequestError({ message: "Invalid db host" });
+ return providerInputs;
+ };
+
+ const getClient = async (providerInputs: z.infer) => {
+ const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
+ const db = knex({
+ client: providerInputs.client,
+ connection: {
+ database: providerInputs.database,
+ port: providerInputs.port,
+ host: providerInputs.host,
+ user: providerInputs.username,
+ password: providerInputs.password,
+ ssl,
+ pool: { min: 0, max: 1 }
+ },
+ acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
+ });
+ return db;
+ };
+
+ const validateConnection = async (inputs: unknown) => {
+ const providerInputs = await validateProviderInputs(inputs);
+ const db = await getClient(providerInputs);
+ // oracle needs from keyword
+ const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
+
+ const isConnected = await db.raw(testStatement).then(() => true);
+ await db.destroy();
+ return isConnected;
+ };
+
+ const create = async (inputs: unknown, expireAt: number) => {
+ const providerInputs = await validateProviderInputs(inputs);
+ const db = await getClient(providerInputs);
+
+ const username = generateUsername(providerInputs.client);
+ const password = generatePassword(providerInputs.client);
+ const { database } = providerInputs;
+ const expiration = new Date(expireAt).toISOString();
+
+ const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
+ username,
+ password,
+ expiration,
+ database
+ });
+
+ await db.transaction(async (tx) =>
+ Promise.all(
+ creationStatement
+ .toString()
+ .split(";")
+ .filter(Boolean)
+ .map((query) => tx.raw(query))
+ )
+ );
+ await db.destroy();
+ return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
+ };
+
+ const revoke = async (inputs: unknown, entityId: string) => {
+ const providerInputs = await validateProviderInputs(inputs);
+ const db = await getClient(providerInputs);
+
+ const username = entityId;
+ const { database } = providerInputs;
+
+ const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
+ await db.transaction(async (tx) =>
+ Promise.all(
+ revokeStatement
+ .toString()
+ .split(";")
+ .filter(Boolean)
+ .map((query) => tx.raw(query))
+ )
+ );
+
+ await db.destroy();
+ return { entityId: username };
+ };
+
+ const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
+ const providerInputs = await validateProviderInputs(inputs);
+ const db = await getClient(providerInputs);
+
+ const username = entityId;
+ const expiration = new Date(expireAt).toISOString();
+ const { database } = providerInputs;
+
+ const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
+ if (renewStatement)
+ await db.transaction(async (tx) =>
+ Promise.all(
+ renewStatement
+ .toString()
+ .split(";")
+ .filter(Boolean)
+ .map((query) => tx.raw(query))
+ )
+ );
+
+ await db.destroy();
+ return { entityId: username };
+ };
+
+ return {
+ validateProviderInputs,
+ validateConnection,
+ create,
+ revoke,
+ renew
+ };
+};
diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts
new file mode 100644
index 000000000..55afd4e10
--- /dev/null
+++ b/backend/src/ee/services/group/group-dal.ts
@@ -0,0 +1,157 @@
+import { Knex } from "knex";
+
+import { TDbClient } from "@app/db";
+import { TableName, TGroups } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
+import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
+
+export type TGroupDALFactory = ReturnType;
+
+export const groupDALFactory = (db: TDbClient) => {
+ const groupOrm = ormify(db, TableName.Groups);
+
+ const findGroups = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => {
+ try {
+ const query = (tx || db)(TableName.Groups)
+ // eslint-disable-next-line
+ .where(buildFindFilter(filter))
+ .select(selectAllTableCols(TableName.Groups));
+
+ if (limit) void query.limit(limit);
+ if (offset) void query.limit(offset);
+ if (sort) {
+ void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
+ }
+
+ const res = await query;
+ return res;
+ } catch (err) {
+ throw new DatabaseError({ error: err, name: "Find groups" });
+ }
+ };
+
+ const findByOrgId = async (orgId: string, tx?: Knex) => {
+ try {
+ const docs = await (tx || db)(TableName.Groups)
+ .where(`${TableName.Groups}.orgId`, orgId)
+ .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
+ .select(selectAllTableCols(TableName.Groups))
+ // cr stands for custom role
+ .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
+ .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
+ .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
+ .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
+ .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles));
+ return docs.map(({ crId, crDescription, crSlug, crPermission, crName, ...el }) => ({
+ ...el,
+ customRole: el.roleId
+ ? {
+ id: crId,
+ name: crName,
+ slug: crSlug,
+ permissions: crPermission,
+ description: crDescription
+ }
+ : undefined
+ }));
+ } catch (error) {
+ throw new DatabaseError({ error, name: "FindByOrgId" });
+ }
+ };
+
+ const countAllGroupMembers = async ({ orgId, groupId }: { orgId: string; groupId: string }) => {
+ try {
+ interface CountResult {
+ count: string;
+ }
+
+ const doc = await db(TableName.OrgMembership)
+ .where(`${TableName.OrgMembership}.orgId`, orgId)
+ .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
+ .leftJoin(TableName.UserGroupMembership, function () {
+ this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
+ `${TableName.UserGroupMembership}.groupId`,
+ "=",
+ db.raw("?", [groupId])
+ );
+ })
+ .where({ isGhost: false })
+ .count(`${TableName.Users}.id`)
+ .first();
+
+ return parseInt((doc?.count as string) || "0", 10);
+ } catch (err) {
+ throw new DatabaseError({ error: err, name: "Count all group members" });
+ }
+ };
+
+ // special query
+ const findAllGroupMembers = async ({
+ orgId,
+ groupId,
+ offset = 0,
+ limit,
+ username
+ }: {
+ orgId: string;
+ groupId: string;
+ offset?: number;
+ limit?: number;
+ username?: string;
+ }) => {
+ try {
+ let query = db(TableName.OrgMembership)
+ .where(`${TableName.OrgMembership}.orgId`, orgId)
+ .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
+ .leftJoin(TableName.UserGroupMembership, function () {
+ this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn(
+ `${TableName.UserGroupMembership}.groupId`,
+ "=",
+ db.raw("?", [groupId])
+ );
+ })
+ .select(
+ db.ref("id").withSchema(TableName.OrgMembership),
+ db.ref("groupId").withSchema(TableName.UserGroupMembership),
+ db.ref("email").withSchema(TableName.Users),
+ db.ref("username").withSchema(TableName.Users),
+ db.ref("firstName").withSchema(TableName.Users),
+ db.ref("lastName").withSchema(TableName.Users),
+ db.ref("id").withSchema(TableName.Users).as("userId")
+ )
+ .where({ isGhost: false })
+ .offset(offset);
+
+ if (limit) {
+ query = query.limit(limit);
+ }
+
+ if (username) {
+ query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
+ }
+
+ const members = await query;
+
+ return members.map(
+ ({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
+ id: userId,
+ email,
+ username: memberUsername,
+ firstName,
+ lastName,
+ isPartOfGroup: !!memberGroupId
+ })
+ );
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Find all org members" });
+ }
+ };
+
+ return {
+ findGroups,
+ findByOrgId,
+ countAllGroupMembers,
+ findAllGroupMembers,
+ ...groupOrm
+ };
+};
diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts
new file mode 100644
index 000000000..285403bcd
--- /dev/null
+++ b/backend/src/ee/services/group/group-service.ts
@@ -0,0 +1,474 @@
+import { ForbiddenError } from "@casl/ability";
+import slugify from "@sindresorhus/slugify";
+
+import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
+import { isAtLeastAsPrivileged } from "@app/lib/casl";
+import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
+import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
+import { alphaNumericNanoId } from "@app/lib/nanoid";
+
+import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-dal";
+import { TOrgDALFactory } from "../../../services/org/org-dal";
+import { TProjectDALFactory } from "../../../services/project/project-dal";
+import { TProjectBotDALFactory } from "../../../services/project-bot/project-bot-dal";
+import { TProjectKeyDALFactory } from "../../../services/project-key/project-key-dal";
+import { TUserDALFactory } from "../../../services/user/user-dal";
+import { TLicenseServiceFactory } from "../license/license-service";
+import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
+import { TPermissionServiceFactory } from "../permission/permission-service";
+import { TGroupDALFactory } from "./group-dal";
+import {
+ TAddUserToGroupDTO,
+ TCreateGroupDTO,
+ TDeleteGroupDTO,
+ TListGroupUsersDTO,
+ TRemoveUserFromGroupDTO,
+ TUpdateGroupDTO
+} from "./group-types";
+import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
+
+type TGroupServiceFactoryDep = {
+ userDAL: Pick;
+ groupDAL: Pick<
+ TGroupDALFactory,
+ "create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countAllGroupMembers"
+ >;
+ groupProjectDAL: Pick;
+ orgDAL: Pick;
+ userGroupMembershipDAL: Pick<
+ TUserGroupMembershipDALFactory,
+ "findOne" | "create" | "delete" | "filterProjectsByUserMembership"
+ >;
+ projectDAL: Pick;
+ projectBotDAL: Pick;
+ projectKeyDAL: Pick;
+ permissionService: Pick;
+ licenseService: Pick;
+};
+
+export type TGroupServiceFactory = ReturnType;
+
+export const groupServiceFactory = ({
+ userDAL,
+ groupDAL,
+ groupProjectDAL,
+ orgDAL,
+ userGroupMembershipDAL,
+ projectDAL,
+ projectBotDAL,
+ projectKeyDAL,
+ permissionService,
+ licenseService
+}: TGroupServiceFactoryDep) => {
+ const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
+ if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
+
+ const plan = await licenseService.getPlan(actorOrgId);
+ if (!plan.groups)
+ throw new BadRequestError({
+ message: "Failed to create group due to plan restriction. Upgrade plan to create group."
+ });
+
+ const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
+ role,
+ actorOrgId
+ );
+ const isCustomRole = Boolean(customRole);
+ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" });
+
+ const group = await groupDAL.create({
+ name,
+ slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
+ orgId: actorOrgId,
+ role: isCustomRole ? OrgMembershipRole.Custom : role,
+ roleId: customRole?.id
+ });
+
+ return group;
+ };
+
+ const updateGroup = async ({
+ currentSlug,
+ name,
+ slug,
+ role,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TUpdateGroupDTO) => {
+ if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
+
+ const plan = await licenseService.getPlan(actorOrgId);
+ if (!plan.groups)
+ throw new BadRequestError({
+ message: "Failed to update group due to plan restrictio Upgrade plan to update group."
+ });
+
+ const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
+ if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
+
+ let customRole: TOrgRoles | undefined;
+ if (role) {
+ const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole(
+ role,
+ group.orgId
+ );
+
+ const isCustomRole = Boolean(customOrgRole);
+ const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasRequiredNewRolePermission)
+ throw new BadRequestError({ message: "Failed to create a more privileged group" });
+ if (isCustomRole) customRole = customOrgRole;
+ }
+
+ const [updatedGroup] = await groupDAL.update(
+ {
+ orgId: actorOrgId,
+ slug: currentSlug
+ },
+ {
+ name,
+ slug: slug ? slugify(slug) : undefined,
+ ...(role
+ ? {
+ role: customRole ? OrgMembershipRole.Custom : role,
+ roleId: customRole?.id ?? null
+ }
+ : {})
+ }
+ );
+
+ return updatedGroup;
+ };
+
+ const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
+ if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
+
+ const plan = await licenseService.getPlan(actorOrgId);
+
+ if (!plan.groups)
+ throw new BadRequestError({
+ message: "Failed to delete group due to plan restriction. Upgrade plan to delete group."
+ });
+
+ const [group] = await groupDAL.delete({
+ orgId: actorOrgId,
+ slug: groupSlug
+ });
+
+ return group;
+ };
+
+ const listGroupUsers = async ({
+ groupSlug,
+ offset,
+ limit,
+ username,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TListGroupUsersDTO) => {
+ if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
+
+ const group = await groupDAL.findOne({
+ orgId: actorOrgId,
+ slug: groupSlug
+ });
+
+ if (!group)
+ throw new BadRequestError({
+ message: `Failed to find group with slug ${groupSlug}`
+ });
+
+ const users = await groupDAL.findAllGroupMembers({
+ orgId: group.orgId,
+ groupId: group.id,
+ offset,
+ limit,
+ username
+ });
+
+ const totalCount = await groupDAL.countAllGroupMembers({
+ orgId: group.orgId,
+ groupId: group.id
+ });
+
+ return { users, totalCount };
+ };
+
+ const addUserToGroup = async ({
+ groupSlug,
+ username,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TAddUserToGroupDTO) => {
+ if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
+
+ // check if group with slug exists
+ const group = await groupDAL.findOne({
+ orgId: actorOrgId,
+ slug: groupSlug
+ });
+
+ if (!group)
+ throw new BadRequestError({
+ message: `Failed to find group with slug ${groupSlug}`
+ });
+
+ const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
+
+ // check if user has broader or equal to privileges than group
+ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
+ if (!hasRequiredPriviledges)
+ throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
+
+ // get user with username
+ const user = await userDAL.findUserEncKeyByUsername({
+ username
+ });
+
+ if (!user)
+ throw new BadRequestError({
+ message: `Failed to find user with username ${username}`
+ });
+
+ // check if user group membership already exists
+ const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
+ groupId: group.id,
+ userId: user.userId
+ });
+
+ if (existingUserGroupMembership)
+ throw new BadRequestError({
+ message: `User ${username} is already part of the group ${groupSlug}`
+ });
+
+ // check if user is even part of the organization
+ const existingUserOrgMembership = await orgDAL.findMembership({
+ userId: user.userId,
+ orgId: actorOrgId
+ });
+
+ if (!existingUserOrgMembership)
+ throw new BadRequestError({
+ message: `User ${username} is not part of the organization`
+ });
+
+ await userGroupMembershipDAL.create({
+ userId: user.userId,
+ groupId: group.id
+ });
+
+ // check which projects the group is part of
+ const projectIds = (
+ await groupProjectDAL.find({
+ groupId: group.id
+ })
+ ).map((gp) => gp.projectId);
+
+ const keys = await projectKeyDAL.find({
+ receiverId: user.userId,
+ $in: {
+ projectId: projectIds
+ }
+ });
+
+ const keysSet = new Set(keys.map((k) => k.projectId));
+ const projectsToAddKeyFor = projectIds.filter((p) => !keysSet.has(p));
+
+ for await (const projectId of projectsToAddKeyFor) {
+ const ghostUser = await projectDAL.findProjectGhostUser(projectId);
+
+ if (!ghostUser) {
+ throw new BadRequestError({
+ message: "Failed to find sudo user"
+ });
+ }
+
+ const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
+
+ if (!ghostUserLatestKey) {
+ throw new BadRequestError({
+ message: "Failed to find sudo user latest key"
+ });
+ }
+
+ const bot = await projectBotDAL.findOne({ projectId });
+
+ if (!bot) {
+ throw new BadRequestError({
+ message: "Failed to find bot"
+ });
+ }
+
+ const botPrivateKey = infisicalSymmetricDecrypt({
+ keyEncoding: bot.keyEncoding as SecretKeyEncoding,
+ iv: bot.iv,
+ tag: bot.tag,
+ ciphertext: bot.encryptedPrivateKey
+ });
+
+ const plaintextProjectKey = decryptAsymmetric({
+ ciphertext: ghostUserLatestKey.encryptedKey,
+ nonce: ghostUserLatestKey.nonce,
+ publicKey: ghostUserLatestKey.sender.publicKey,
+ privateKey: botPrivateKey
+ });
+
+ const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, user.publicKey, botPrivateKey);
+
+ await projectKeyDAL.create({
+ encryptedKey,
+ nonce,
+ senderId: ghostUser.id,
+ receiverId: user.userId,
+ projectId
+ });
+ }
+
+ return user;
+ };
+
+ const removeUserFromGroup = async ({
+ groupSlug,
+ username,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TRemoveUserFromGroupDTO) => {
+ if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
+
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
+
+ // check if group with slug exists
+ const group = await groupDAL.findOne({
+ orgId: actorOrgId,
+ slug: groupSlug
+ });
+
+ if (!group)
+ throw new BadRequestError({
+ message: `Failed to find group with slug ${groupSlug}`
+ });
+
+ const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
+
+ // check if user has broader or equal to privileges than group
+ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
+ if (!hasRequiredPriviledges)
+ throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
+
+ const user = await userDAL.findOne({
+ username
+ });
+
+ if (!user)
+ throw new BadRequestError({
+ message: `Failed to find user with username ${username}`
+ });
+
+ // check if user group membership already exists
+ const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
+ groupId: group.id,
+ userId: user.id
+ });
+
+ if (!existingUserGroupMembership)
+ throw new BadRequestError({
+ message: `User ${username} is not part of the group ${groupSlug}`
+ });
+
+ const projectIds = (
+ await groupProjectDAL.find({
+ groupId: group.id
+ })
+ ).map((gp) => gp.projectId);
+
+ const t = await userGroupMembershipDAL.filterProjectsByUserMembership(user.id, group.id, projectIds);
+
+ const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
+
+ if (projectsToDeleteKeyFor.length) {
+ await projectKeyDAL.delete({
+ receiverId: user.id,
+ $in: {
+ projectId: projectsToDeleteKeyFor
+ }
+ });
+ }
+
+ await userGroupMembershipDAL.delete({
+ groupId: group.id,
+ userId: user.id
+ });
+
+ return user;
+ };
+
+ return {
+ createGroup,
+ updateGroup,
+ deleteGroup,
+ listGroupUsers,
+ addUserToGroup,
+ removeUserFromGroup
+ };
+};
diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts
new file mode 100644
index 000000000..e2fbbe63e
--- /dev/null
+++ b/backend/src/ee/services/group/group-types.ts
@@ -0,0 +1,37 @@
+import { TGenericPermission } from "@app/lib/types";
+
+export type TCreateGroupDTO = {
+ name: string;
+ slug?: string;
+ role: string;
+} & TGenericPermission;
+
+export type TUpdateGroupDTO = {
+ currentSlug: string;
+} & Partial<{
+ name: string;
+ slug: string;
+ role: string;
+}> &
+ TGenericPermission;
+
+export type TDeleteGroupDTO = {
+ groupSlug: string;
+} & TGenericPermission;
+
+export type TListGroupUsersDTO = {
+ groupSlug: string;
+ offset: number;
+ limit: number;
+ username?: string;
+} & TGenericPermission;
+
+export type TAddUserToGroupDTO = {
+ groupSlug: string;
+ username: string;
+} & TGenericPermission;
+
+export type TRemoveUserFromGroupDTO = {
+ groupSlug: string;
+ username: string;
+} & TGenericPermission;
diff --git a/backend/src/ee/services/group/user-group-membership-dal.ts b/backend/src/ee/services/group/user-group-membership-dal.ts
new file mode 100644
index 000000000..e8a262c3e
--- /dev/null
+++ b/backend/src/ee/services/group/user-group-membership-dal.ts
@@ -0,0 +1,125 @@
+import { TDbClient } from "@app/db";
+import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
+import { ormify } from "@app/lib/knex";
+
+export type TUserGroupMembershipDALFactory = ReturnType;
+
+export const userGroupMembershipDALFactory = (db: TDbClient) => {
+ const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
+
+ /**
+ * Returns a sub-set of projectIds fed into this function corresponding to projects where either:
+ * - The user is a direct member of the project.
+ * - The user is a member of a group that is a member of the project, excluding projects that they are part of
+ * through the group with id [groupId].
+ */
+ const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => {
+ const userProjectMemberships: string[] = await db(TableName.ProjectMembership)
+ .where(`${TableName.ProjectMembership}.userId`, userId)
+ .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
+ .pluck(`${TableName.ProjectMembership}.projectId`);
+
+ const userGroupMemberships: string[] = await db(TableName.UserGroupMembership)
+ .where(`${TableName.UserGroupMembership}.userId`, userId)
+ .whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
+ .join(
+ TableName.GroupProjectMembership,
+ `${TableName.UserGroupMembership}.groupId`,
+ `${TableName.GroupProjectMembership}.groupId`
+ )
+ .whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
+ .pluck(`${TableName.GroupProjectMembership}.projectId`);
+
+ return new Set(userProjectMemberships.concat(userGroupMemberships));
+ };
+
+ // special query
+ const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
+ try {
+ const usernameDocs: string[] = await db(TableName.UserGroupMembership)
+ .join(
+ TableName.GroupProjectMembership,
+ `${TableName.UserGroupMembership}.groupId`,
+ `${TableName.GroupProjectMembership}.groupId`
+ )
+ .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
+ .where(`${TableName.GroupProjectMembership}.projectId`, projectId)
+ .whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
+ .pluck(`${TableName.Users}.id`);
+
+ return usernameDocs;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Find user group members in project" });
+ }
+ };
+
+ /**
+ * Return list of users that are part of the group with id [groupId]
+ * that have not yet been added individually to project with id [projectId].
+ *
+ * Note: Filters out users that are part of other groups in the project.
+ * @param groupId
+ * @param projectId
+ * @returns
+ */
+ const findGroupMembersNotInProject = async (groupId: string, projectId: string) => {
+ try {
+ // get list of groups in the project with id [projectId]
+ // that that are not the group with id [groupId]
+ const groups: string[] = await db(TableName.GroupProjectMembership)
+ .where(`${TableName.GroupProjectMembership}.projectId`, projectId)
+ .whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId)
+ .pluck(`${TableName.GroupProjectMembership}.groupId`);
+
+ // main query
+ const members = await db(TableName.UserGroupMembership)
+ .where(`${TableName.UserGroupMembership}.groupId`, groupId)
+ .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
+ .leftJoin(TableName.ProjectMembership, function () {
+ this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
+ `${TableName.ProjectMembership}.projectId`,
+ "=",
+ db.raw("?", [projectId])
+ );
+ })
+ .whereNull(`${TableName.ProjectMembership}.userId`)
+ .leftJoin(
+ TableName.UserEncryptionKey,
+ `${TableName.UserEncryptionKey}.userId`,
+ `${TableName.Users}.id`
+ )
+ .select(
+ db.ref("id").withSchema(TableName.UserGroupMembership),
+ db.ref("groupId").withSchema(TableName.UserGroupMembership),
+ db.ref("email").withSchema(TableName.Users),
+ db.ref("username").withSchema(TableName.Users),
+ db.ref("firstName").withSchema(TableName.Users),
+ db.ref("lastName").withSchema(TableName.Users),
+ db.ref("id").withSchema(TableName.Users).as("userId"),
+ db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
+ )
+ .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
+ .whereNotIn(`${TableName.UserGroupMembership}.userId`, function () {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this.select(`${TableName.UserGroupMembership}.userId`)
+ .from(TableName.UserGroupMembership)
+ .whereIn(`${TableName.UserGroupMembership}.groupId`, groups);
+ });
+
+ return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
+ ...data,
+ user: { email, username, firstName, lastName, id: userId, publicKey }
+ }));
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Find group members not in project" });
+ }
+ };
+
+ return {
+ ...userGroupMembershipOrm,
+ filterProjectsByUserMembership,
+ findUserGroupMembershipsInProject,
+ findGroupMembersNotInProject
+ };
+};
diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts
new file mode 100644
index 000000000..26252f2d1
--- /dev/null
+++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts
@@ -0,0 +1,12 @@
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { ormify } from "@app/lib/knex";
+
+export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType<
+ typeof identityProjectAdditionalPrivilegeDALFactory
+>;
+
+export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => {
+ const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege);
+ return orm;
+};
diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts
new file mode 100644
index 000000000..81dc11a00
--- /dev/null
+++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts
@@ -0,0 +1,297 @@
+import { ForbiddenError } from "@casl/ability";
+import ms from "ms";
+
+import { isAtLeastAsPrivileged } from "@app/lib/casl";
+import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
+import { ActorType } from "@app/services/auth/auth-type";
+import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
+
+import { TPermissionServiceFactory } from "../permission/permission-service";
+import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
+import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
+import {
+ IdentityProjectAdditionalPrivilegeTemporaryMode,
+ TCreateIdentityPrivilegeDTO,
+ TDeleteIdentityPrivilegeDTO,
+ TGetIdentityPrivilegeDetailsDTO,
+ TListIdentityPrivilegesDTO,
+ TUpdateIdentityPrivilegeDTO
+} from "./identity-project-additional-privilege-types";
+
+type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
+ identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
+ identityProjectDAL: Pick;
+ projectDAL: Pick;
+ permissionService: Pick;
+};
+
+export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
+ typeof identityProjectAdditionalPrivilegeServiceFactory
+>;
+
+export const identityProjectAdditionalPrivilegeServiceFactory = ({
+ identityProjectAdditionalPrivilegeDAL,
+ identityProjectDAL,
+ permissionService,
+ projectDAL
+}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => {
+ const create = async ({
+ slug,
+ actor,
+ actorId,
+ identityId,
+ projectSlug,
+ permissions: customPermission,
+ actorOrgId,
+ actorAuthMethod,
+ ...dto
+ }: TCreateIdentityPrivilegeDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+ const projectId = project.id;
+
+ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
+ if (!identityProjectMembership)
+ throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
+ const { permission: identityRolePermission } = await permissionService.getProjectPermission(
+ ActorType.IDENTITY,
+ identityId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
+ if (!hasRequiredPriviledges)
+ throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
+
+ const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
+ slug,
+ projectMembershipId: identityProjectMembership.id
+ });
+ if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
+
+ if (!dto.isTemporary) {
+ const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
+ projectMembershipId: identityProjectMembership.id,
+ slug,
+ permissions: customPermission
+ });
+ return additionalPrivilege;
+ }
+
+ const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
+ const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
+ projectMembershipId: identityProjectMembership.id,
+ slug,
+ permissions: customPermission,
+ isTemporary: true,
+ temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
+ temporaryRange: dto.temporaryRange,
+ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
+ temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
+ });
+ return additionalPrivilege;
+ };
+
+ const updateBySlug = async ({
+ projectSlug,
+ slug,
+ identityId,
+ data,
+ actorOrgId,
+ actor,
+ actorId,
+ actorAuthMethod
+ }: TUpdateIdentityPrivilegeDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+ const projectId = project.id;
+
+ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
+ if (!identityProjectMembership)
+ throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
+ const { permission: identityRolePermission } = await permissionService.getProjectPermission(
+ ActorType.IDENTITY,
+ identityProjectMembership.identityId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
+ if (!hasRequiredPriviledges)
+ throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" });
+
+ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
+ slug,
+ projectMembershipId: identityProjectMembership.id
+ });
+ if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
+ if (data?.slug) {
+ const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
+ slug: data.slug,
+ projectMembershipId: identityProjectMembership.id
+ });
+ if (existingSlug && existingSlug.id !== identityPrivilege.id)
+ throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
+ }
+
+ const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
+ if (isTemporary) {
+ const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
+ const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
+ const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
+ ...data,
+ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
+ temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
+ });
+ return additionalPrivilege;
+ }
+
+ const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
+ ...data,
+ isTemporary: false,
+ temporaryAccessStartTime: null,
+ temporaryAccessEndTime: null,
+ temporaryRange: null,
+ temporaryMode: null
+ });
+ return additionalPrivilege;
+ };
+
+ const deleteBySlug = async ({
+ actorId,
+ slug,
+ identityId,
+ projectSlug,
+ actor,
+ actorOrgId,
+ actorAuthMethod
+ }: TDeleteIdentityPrivilegeDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+ const projectId = project.id;
+
+ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
+ if (!identityProjectMembership)
+ throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
+ const { permission: identityRolePermission } = await permissionService.getProjectPermission(
+ ActorType.IDENTITY,
+ identityProjectMembership.identityId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
+ if (!hasRequiredPriviledges)
+ throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" });
+
+ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
+ slug,
+ projectMembershipId: identityProjectMembership.id
+ });
+ if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
+
+ const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
+ return deletedPrivilege;
+ };
+
+ const getPrivilegeDetailsBySlug = async ({
+ projectSlug,
+ identityId,
+ slug,
+ actorOrgId,
+ actor,
+ actorId,
+ actorAuthMethod
+ }: TGetIdentityPrivilegeDetailsDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+ const projectId = project.id;
+
+ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
+ if (!identityProjectMembership)
+ throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
+
+ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
+ slug,
+ projectMembershipId: identityProjectMembership.id
+ });
+ if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
+
+ return identityPrivilege;
+ };
+
+ const listIdentityProjectPrivileges = async ({
+ identityId,
+ actorOrgId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ projectSlug
+ }: TListIdentityPrivilegesDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+ if (!project) throw new BadRequestError({ message: "Project not found" });
+ const projectId = project.id;
+
+ const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
+ if (!identityProjectMembership)
+ throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` });
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ identityProjectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
+
+ const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
+ projectMembershipId: identityProjectMembership.id
+ });
+ return identityPrivileges;
+ };
+
+ return {
+ create,
+ updateBySlug,
+ deleteBySlug,
+ getPrivilegeDetailsBySlug,
+ listIdentityProjectPrivileges
+ };
+};
diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts
new file mode 100644
index 000000000..88ff01d7d
--- /dev/null
+++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts
@@ -0,0 +1,54 @@
+import { TProjectPermission } from "@app/lib/types";
+
+export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
+ Relative = "relative"
+}
+
+export type TCreateIdentityPrivilegeDTO = {
+ permissions: unknown;
+ identityId: string;
+ projectSlug: string;
+ slug: string;
+} & (
+ | {
+ isTemporary: false;
+ }
+ | {
+ isTemporary: true;
+ temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ }
+) &
+ Omit;
+
+export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit<
+ TProjectPermission,
+ "projectId"
+> & {
+ data: Partial<{
+ permissions: unknown;
+ slug: string;
+ isTemporary: boolean;
+ temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ }>;
+ };
+
+export type TDeleteIdentityPrivilegeDTO = Omit & {
+ slug: string;
+ identityId: string;
+ projectSlug: string;
+};
+
+export type TGetIdentityPrivilegeDetailsDTO = Omit & {
+ slug: string;
+ identityId: string;
+ projectSlug: string;
+};
+
+export type TListIdentityPrivilegesDTO = Omit & {
+ identityId: string;
+ projectSlug: string;
+};
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 e9ae0264a..76e2d40ce 100644
--- a/backend/src/ee/services/ldap-config/ldap-config-service.ts
+++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts
@@ -12,7 +12,6 @@ import {
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
-import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -24,7 +23,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TLdapConfigDALFactory } from "./ldap-config-dal";
-import { TCreateLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
+import { TCreateLdapCfgDTO, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types";
type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: TLdapConfigDALFactory;
@@ -55,6 +54,7 @@ export const ldapConfigServiceFactory = ({
actorId,
orgId,
actorOrgId,
+ actorAuthMethod,
isActive,
url,
bindDN,
@@ -62,7 +62,7 @@ export const ldapConfigServiceFactory = ({
searchBase,
caCert
}: TCreateLdapCfgDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId);
@@ -149,13 +149,14 @@ export const ldapConfigServiceFactory = ({
orgId,
actorOrgId,
isActive,
+ actorAuthMethod,
url,
bindDN,
bindPass,
searchBase,
caCert
}: TUpdateLdapCfgDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId);
@@ -274,8 +275,14 @@ export const ldapConfigServiceFactory = ({
};
};
- const getLdapCfgWithPermissionCheck = async ({ actor, actorId, orgId, actorOrgId }: TOrgPermission) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getLdapCfgWithPermissionCheck = async ({
+ actor,
+ actorId,
+ orgId,
+ actorAuthMethod,
+ actorOrgId
+ }: TGetLdapCfgDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
return getLdapCfg({
orgId
diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts
index 025ce7781..4e261f9e9 100644
--- a/backend/src/ee/services/ldap-config/ldap-config-types.ts
+++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts
@@ -1,6 +1,7 @@
import { TOrgPermission } from "@app/lib/types";
export type TCreateLdapCfgDTO = {
+ orgId: string;
isActive: boolean;
url: string;
bindDN: string;
@@ -9,7 +10,9 @@ export type TCreateLdapCfgDTO = {
caCert: string;
} & TOrgPermission;
-export type TUpdateLdapCfgDTO = Partial<{
+export type TUpdateLdapCfgDTO = {
+ orgId: string;
+} & Partial<{
isActive: boolean;
url: string;
bindDN: string;
@@ -19,6 +22,10 @@ export type TUpdateLdapCfgDTO = Partial<{
}> &
TOrgPermission;
+export type TGetLdapCfgDTO = {
+ orgId: string;
+} & TOrgPermission;
+
export type TLdapLoginDTO = {
externalId: string;
username: string;
diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts
index 8f52939c5..b5cbf103e 100644
--- a/backend/src/ee/services/license/__mocks__/licence-fns.ts
+++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts
@@ -20,6 +20,7 @@ export const getDefaultOnPremFeatures = () => {
samlSSO: false,
scim: false,
ldap: false,
+ groups: false,
status: null,
trial_end: null,
has_used_trial: true,
diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts
index 8dca96737..8a4de57f1 100644
--- a/backend/src/ee/services/license/licence-fns.ts
+++ b/backend/src/ee/services/license/licence-fns.ts
@@ -15,6 +15,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
+ dynamicSecret: false,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
@@ -26,6 +27,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
samlSSO: false,
scim: false,
ldap: false,
+ groups: false,
status: null,
trial_end: null,
has_used_trial: true,
diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts
index 49609e8c9..e81f6dc12 100644
--- a/backend/src/ee/services/license/license-service.ts
+++ b/backend/src/ee/services/license/license-service.ts
@@ -8,6 +8,7 @@ import { ForbiddenError } from "@casl/ability";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
+import { verifyOfflineLicense } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -26,6 +27,7 @@ import {
TFeatureSet,
TGetOrgBillInfoDTO,
TGetOrgTaxIdDTO,
+ TOfflineLicenseContents,
TOrgInvoiceDTO,
TOrgLicensesDTO,
TOrgPlanDTO,
@@ -96,6 +98,36 @@ export const licenseServiceFactory = ({
}
return;
}
+
+ if (appCfg.LICENSE_KEY_OFFLINE) {
+ let isValidOfflineLicense = true;
+ const contents: TOfflineLicenseContents = JSON.parse(
+ Buffer.from(appCfg.LICENSE_KEY_OFFLINE, "base64").toString("utf8")
+ );
+ const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature);
+
+ if (!isVerified) {
+ isValidOfflineLicense = false;
+ logger.warn(`Infisical EE offline license verification failed`);
+ }
+
+ if (contents.license.terminatesAt) {
+ const terminationDate = new Date(contents.license.terminatesAt);
+ if (terminationDate < new Date()) {
+ isValidOfflineLicense = false;
+ logger.warn(`Infisical EE offline license has expired`);
+ }
+ }
+
+ if (isValidOfflineLicense) {
+ onPremFeatures = contents.license.features;
+ instanceType = InstanceType.EnterpriseOnPrem;
+ logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
+ isValidLicense = true;
+ return;
+ }
+ }
+
// this means this is self hosted oss version
// else it would reach catch statement
isValidLicense = true;
@@ -192,9 +224,10 @@ export const licenseServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
billingCycle
}: TOrgPlansTableDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
@@ -202,15 +235,22 @@ export const licenseServiceFactory = ({
return data;
};
- const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, projectId }: TOrgPlanDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const plan = await getPlan(orgId, projectId);
return plan;
};
- const startOrgTrial = async ({ orgId, actorId, actor, actorOrgId, success_url }: TStartOrgTrialDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const startOrgTrial = async ({
+ orgId,
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ success_url
+ }: TStartOrgTrialDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
@@ -231,8 +271,14 @@ export const licenseServiceFactory = ({
return { url };
};
- const createOrganizationPortalSession = async ({ orgId, actorId, actor, actorOrgId }: TCreateOrgPortalSession) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const createOrganizationPortalSession = async ({
+ orgId,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TCreateOrgPortalSession) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
@@ -278,8 +324,8 @@ export const licenseServiceFactory = ({
return { url };
};
- const getOrgBillingInfo = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -295,8 +341,8 @@ export const licenseServiceFactory = ({
};
// returns org current plan feature table
- const getOrgPlanTable = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -311,8 +357,8 @@ export const licenseServiceFactory = ({
return data;
};
- const getOrgBillingDetails = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -332,11 +378,12 @@ export const licenseServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
orgId,
name,
email
}: TUpdateOrgBillingDetailsDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -355,8 +402,8 @@ export const licenseServiceFactory = ({
return data;
};
- const getOrgPmtMethods = async ({ orgId, actor, actorId, actorOrgId }: TOrgPmtMethodsDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -378,11 +425,12 @@ export const licenseServiceFactory = ({
orgId,
actor,
actorId,
+ actorAuthMethod,
actorOrgId,
success_url,
cancel_url
}: TAddOrgPmtMethodDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -403,8 +451,15 @@ export const licenseServiceFactory = ({
return { url };
};
- const delOrgPmtMethods = async ({ actorId, actor, actorOrgId, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const delOrgPmtMethods = async ({
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId,
+ orgId,
+ pmtMethodId
+ }: TDelOrgPmtMethodDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -420,8 +475,8 @@ export const licenseServiceFactory = ({
return data;
};
- const getOrgTaxIds = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgTaxIdDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -438,8 +493,8 @@ export const licenseServiceFactory = ({
return taxIds;
};
- const addOrgTaxId = async ({ actorId, actor, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -459,8 +514,8 @@ export const licenseServiceFactory = ({
return data;
};
- const delOrgTaxId = async ({ orgId, actor, actorId, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -476,8 +531,8 @@ export const licenseServiceFactory = ({
return data;
};
- const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, orgId }: TOrgInvoiceDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
@@ -493,8 +548,8 @@ export const licenseServiceFactory = ({
return invoices;
};
- const getOrgLicenses = async ({ orgId, actor, actorId, actorOrgId }: TOrgLicensesDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts
index 80f422380..1cea39a83 100644
--- a/backend/src/ee/services/license/license-types.ts
+++ b/backend/src/ee/services/license/license-types.ts
@@ -6,12 +6,28 @@ export enum InstanceType {
Cloud = "cloud"
}
+export type TOfflineLicenseContents = {
+ license: TOfflineLicense;
+ signature: string;
+};
+
+export type TOfflineLicense = {
+ issuedTo: string;
+ licenseId: string;
+ customerId: string | null;
+ issuedAt: string;
+ expiresAt: string | null;
+ terminatesAt: string | null;
+ features: TFeatureSet;
+};
+
export type TFeatureSet = {
_id: null;
slug: null;
tier: -1;
workspaceLimit: null;
workspacesUsed: 0;
+ dynamicSecret: false;
memberLimit: null;
membersUsed: 0;
environmentLimit: null;
@@ -27,6 +43,7 @@ export type TFeatureSet = {
samlSSO: false;
scim: false;
ldap: false;
+ groups: false;
status: null;
trial_end: null;
has_used_trial: true;
diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts
index 30b601c2c..9fece040b 100644
--- a/backend/src/ee/services/permission/org-permission.ts
+++ b/backend/src/ee/services/permission/org-permission.ts
@@ -18,6 +18,7 @@ export enum OrgPermissionSubjects {
Sso = "sso",
Scim = "scim",
Ldap = "ldap",
+ Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
@@ -33,6 +34,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
+ | [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
@@ -83,6 +85,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
+ can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
+ can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
+ can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
+ can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
+
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
@@ -105,6 +112,7 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
+ can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts
index d94589b43..d8114388e 100644
--- a/backend/src/ee/services/permission/permission-dal.ts
+++ b/backend/src/ee/services/permission/permission-dal.ts
@@ -45,6 +45,42 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => {
try {
+ const groups: string[] = await db(TableName.GroupProjectMembership)
+ .where(`${TableName.GroupProjectMembership}.projectId`, projectId)
+ .pluck(`${TableName.GroupProjectMembership}.groupId`);
+
+ const groupDocs = await db(TableName.UserGroupMembership)
+ .where(`${TableName.UserGroupMembership}.userId`, userId)
+ .whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
+ .join(
+ TableName.GroupProjectMembership,
+ `${TableName.GroupProjectMembership}.groupId`,
+ `${TableName.UserGroupMembership}.groupId`
+ )
+ .join(
+ TableName.GroupProjectMembershipRole,
+ `${TableName.GroupProjectMembershipRole}.projectMembershipId`,
+ `${TableName.GroupProjectMembership}.id`
+ )
+ .leftJoin(
+ TableName.ProjectRoles,
+ `${TableName.GroupProjectMembershipRole}.customRoleId`,
+ `${TableName.ProjectRoles}.id`
+ )
+ .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
+ .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
+ .select(selectAllTableCols(TableName.GroupProjectMembershipRole))
+ .select(
+ db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
+ db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
+ db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
+ db.ref("projectId").withSchema(TableName.GroupProjectMembership),
+ db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
+ db.ref("orgId").withSchema(TableName.Project),
+ db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
+ )
+ .select("permissions");
+
const docs = await db(TableName.ProjectMembership)
.join(
TableName.ProjectUserMembershipRole,
@@ -56,6 +92,11 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
+ .leftJoin(
+ TableName.ProjectUserAdditionalPrivilege,
+ `${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`,
+ `${TableName.ProjectMembership}.id`
+ )
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId)
@@ -63,31 +104,35 @@ export const permissionDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
.select(
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
- // TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
- db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"),
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
+ db.ref("projectId").withSchema(TableName.ProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
- db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
- )
- .select("permissions");
+ db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
+ db.ref("permissions").withSchema(TableName.ProjectRoles),
+ db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
+ db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
+ db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
+ db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
+ db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
+ db
+ .ref("temporaryAccessStartTime")
+ .withSchema(TableName.ProjectUserAdditionalPrivilege)
+ .as("userApTemporaryAccessStartTime"),
+ db
+ .ref("temporaryAccessEndTime")
+ .withSchema(TableName.ProjectUserAdditionalPrivilege)
+ .as("userApTemporaryAccessEndTime")
+ );
const permission = sqlNestRelationships({
data: docs,
- key: "membershipId",
- parentMapper: ({
- orgId,
- orgAuthEnforced,
- membershipId,
- membershipCreatedAt,
- membershipUpdatedAt,
- oldRoleField
- }) => ({
+ key: "projectId",
+ parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
orgId,
orgAuthEnforced,
userId,
- role: oldRoleField,
id: membershipId,
projectId,
createdAt: membershipCreatedAt,
@@ -102,15 +147,83 @@ export const permissionDALFactory = (db: TDbClient) => {
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
+ },
+ {
+ key: "userApId",
+ label: "additionalPrivileges" as const,
+ mapper: ({
+ userApId,
+ userApPermissions,
+ userApIsTemporary,
+ userApTemporaryMode,
+ userApTemporaryRange,
+ userApTemporaryAccessEndTime,
+ userApTemporaryAccessStartTime
+ }) => ({
+ id: userApId,
+ permissions: userApPermissions,
+ temporaryRange: userApTemporaryRange,
+ temporaryMode: userApTemporaryMode,
+ temporaryAccessEndTime: userApTemporaryAccessEndTime,
+ temporaryAccessStartTime: userApTemporaryAccessStartTime,
+ isTemporary: userApIsTemporary
+ })
}
]
});
+
+ const groupPermission = groupDocs.length
+ ? sqlNestRelationships({
+ data: groupDocs,
+ key: "projectId",
+ parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
+ orgId,
+ orgAuthEnforced,
+ userId,
+ id: membershipId,
+ projectId,
+ createdAt: membershipCreatedAt,
+ updatedAt: membershipUpdatedAt
+ }),
+ childrenMapper: [
+ {
+ key: "id",
+ label: "roles" as const,
+ mapper: (data) =>
+ ProjectUserMembershipRolesSchema.extend({
+ permissions: z.unknown(),
+ customRoleSlug: z.string().optional().nullable()
+ }).parse(data)
+ }
+ ]
+ })
+ : [];
+
+ if (!permission?.[0] && !groupPermission[0]) return undefined;
+
// when introducting cron mode change it here
- const activeRoles = permission?.[0]?.roles.filter(
+ const activeRoles =
+ permission?.[0]?.roles?.filter(
+ ({ isTemporary, temporaryAccessEndTime }) =>
+ !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
+ ) ?? [];
+
+ const activeGroupRoles =
+ groupPermission?.[0]?.roles?.filter(
+ ({ isTemporary, temporaryAccessEndTime }) =>
+ !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
+ ) ?? [];
+
+ const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
- return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
+
+ return {
+ ...(permission[0] || groupPermission[0]),
+ roles: [...activeRoles, ...activeGroupRoles],
+ additionalPrivileges: activeAdditionalPrivileges
+ };
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" });
}
@@ -129,31 +242,60 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
+ .leftJoin(
+ TableName.IdentityProjectAdditionalPrivilege,
+ `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`,
+ `${TableName.IdentityProjectMembership}.id`
+ )
+ .join(
+ // Join the Project table to later select orgId
+ TableName.Project,
+ `${TableName.IdentityProjectMembership}.projectId`,
+ `${TableName.Project}.id`
+ )
.where("identityId", identityId)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
- db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"),
+ db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
- db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
- )
- .select("permissions");
+ db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
+ db.ref("permissions").withSchema(TableName.ProjectRoles),
+ db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
+ db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
+ db
+ .ref("temporaryMode")
+ .withSchema(TableName.IdentityProjectAdditionalPrivilege)
+ .as("identityApTemporaryMode"),
+ db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"),
+ db
+ .ref("temporaryRange")
+ .withSchema(TableName.IdentityProjectAdditionalPrivilege)
+ .as("identityApTemporaryRange"),
+ db
+ .ref("temporaryAccessStartTime")
+ .withSchema(TableName.IdentityProjectAdditionalPrivilege)
+ .as("identityApTemporaryAccessStartTime"),
+ db
+ .ref("temporaryAccessEndTime")
+ .withSchema(TableName.IdentityProjectAdditionalPrivilege)
+ .as("identityApTemporaryAccessEndTime")
+ );
const permission = sqlNestRelationships({
data: docs,
key: "membershipId",
- parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField }) => ({
+ parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
id: membershipId,
identityId,
projectId,
- role: oldRoleField,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
+ orgId,
// just a prefilled value
- orgAuthEnforced: false,
- orgId: ""
+ orgAuthEnforced: false
}),
childrenMapper: [
{
@@ -164,16 +306,44 @@ export const permissionDALFactory = (db: TDbClient) => {
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
+ },
+ {
+ key: "identityApId",
+ label: "additionalPrivileges" as const,
+ mapper: ({
+ identityApId,
+ identityApPermissions,
+ identityApIsTemporary,
+ identityApTemporaryMode,
+ identityApTemporaryRange,
+ identityApTemporaryAccessEndTime,
+ identityApTemporaryAccessStartTime
+ }) => ({
+ id: identityApId,
+ permissions: identityApPermissions,
+ temporaryRange: identityApTemporaryRange,
+ temporaryMode: identityApTemporaryMode,
+ temporaryAccessEndTime: identityApTemporaryAccessEndTime,
+ temporaryAccessStartTime: identityApTemporaryAccessStartTime,
+ isTemporary: identityApIsTemporary
+ })
}
]
});
+ if (!permission?.[0]) return undefined;
+
// when introducting cron mode change it here
const activeRoles = permission?.[0]?.roles.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
- return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
+ const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
+ ({ isTemporary, temporaryAccessEndTime }) =>
+ !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
+ );
+
+ return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectIdentityPermission" });
}
diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts
new file mode 100644
index 000000000..eda19c215
--- /dev/null
+++ b/backend/src/ee/services/permission/permission-fns.ts
@@ -0,0 +1,27 @@
+import { TOrganizations } from "@app/db/schemas";
+import { UnauthorizedError } from "@app/lib/errors";
+import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
+
+function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
+ if (!actorAuthMethod) return false;
+
+ return [
+ AuthMethod.AZURE_SAML,
+ AuthMethod.OKTA_SAML,
+ AuthMethod.JUMPCLOUD_SAML,
+ AuthMethod.GOOGLE_SAML,
+ AuthMethod.KEYCLOAK_SAML
+ ].includes(actorAuthMethod);
+}
+
+function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) {
+ if (actorAuthMethod === undefined) {
+ throw new UnauthorizedError({ name: "No auth method defined" });
+ }
+
+ if (isSamlEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod)) {
+ throw new UnauthorizedError({ name: "Cannot access org-scoped resource" });
+ }
+}
+
+export { isAuthMethodSaml, validateOrgSAML };
diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts
index c7dcf4b8c..f4e423797 100644
--- a/backend/src/ee/services/permission/permission-service.ts
+++ b/backend/src/ee/services/permission/permission-service.ts
@@ -11,13 +11,15 @@ import {
} from "@app/db/schemas";
import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
-import { ActorType } from "@app/services/auth/auth-type";
+import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
+import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal";
import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal";
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
+import { validateOrgSAML } from "./permission-fns";
import { TBuildProjectPermissionDTO } from "./permission-types";
import {
buildServiceTokenProjectPermission,
@@ -32,6 +34,7 @@ type TPermissionServiceFactoryDep = {
orgRoleDAL: Pick;
projectRoleDAL: Pick;
serviceTokenDAL: Pick;
+ projectDAL: Pick;
permissionDAL: TPermissionDALFactory;
};
@@ -41,7 +44,8 @@ export const permissionServiceFactory = ({
permissionDAL,
orgRoleDAL,
projectRoleDAL,
- serviceTokenDAL
+ serviceTokenDAL,
+ projectDAL
}: TPermissionServiceFactoryDep) => {
const buildOrgPermission = (role: string, permission?: unknown) => {
switch (role) {
@@ -98,16 +102,30 @@ export const permissionServiceFactory = ({
/*
* Get user permission in an organization
- * */
- const getUserOrgPermission = async (userId: string, orgId: string, userOrgId?: string) => {
+ */
+ const getUserOrgPermission = async (
+ userId: string,
+ orgId: string,
+ authMethod: ActorAuthMethod,
+ userOrgId?: string
+ ) => {
const membership = await permissionDAL.getOrgPermission(userId, orgId);
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" });
}
- if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
- throw new BadRequestError({ name: "Cannot access org-scoped resource" });
+
+ // If the org ID is API_KEY, the request is being made with an API Key.
+ // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization.
+
+ // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
+ // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
+ if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) {
+ throw new UnauthorizedError({ name: "You are not logged into this organization" });
}
+
+ validateOrgSAML(authMethod, membership.orgAuthEnforced);
+
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
};
@@ -120,10 +138,16 @@ export const permissionServiceFactory = ({
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
};
- const getOrgPermission = async (type: ActorType, id: string, orgId: string, actorOrgId?: string) => {
+ const getOrgPermission = async (
+ type: ActorType,
+ id: string,
+ orgId: string,
+ authMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
switch (type) {
case ActorType.USER:
- return getUserOrgPermission(id, orgId, actorOrgId);
+ return getUserOrgPermission(id, orgId, authMethod, actorOrgId);
case ActorType.IDENTITY:
return getIdentityOrgPermission(id, orgId);
default:
@@ -153,6 +177,7 @@ export const permissionServiceFactory = ({
const getUserProjectPermission = async (
userId: string,
projectId: string,
+ authMethod: ActorAuthMethod,
userOrgId?: string
): Promise> => {
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
@@ -164,12 +189,27 @@ export const permissionServiceFactory = ({
throw new BadRequestError({ name: "Custom permission not found" });
}
- if (userProjectPermission.orgAuthEnforced && userProjectPermission.orgId !== userOrgId) {
- throw new BadRequestError({ name: "Cannot access org-scoped resource" });
+ // If the org ID is API_KEY, the request is being made with an API Key.
+ // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization.
+
+ // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
+ // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
+ if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) {
+ throw new UnauthorizedError({ name: "You are not logged into this organization" });
}
+ validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced);
+
+ // join two permissions and pass to build the final permission set
+ const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
+ const additionalPrivileges =
+ userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
+ role: ProjectMembershipRole.Custom,
+ permissions
+ })) || [];
+
return {
- permission: buildProjectPermission(userProjectPermission.roles),
+ permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
membership: userProjectPermission,
hasRole: (role: string) =>
userProjectPermission.roles.findIndex(
@@ -180,7 +220,8 @@ export const permissionServiceFactory = ({
const getIdentityProjectPermission = async (
identityId: string,
- projectId: string
+ projectId: string,
+ identityOrgId: string | undefined
): Promise> => {
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" });
@@ -193,8 +234,20 @@ export const permissionServiceFactory = ({
throw new BadRequestError({ name: "Custom permission not found" });
}
+ if (identityProjectPermission.orgId !== identityOrgId) {
+ throw new UnauthorizedError({ name: "You are not a member of this organization" });
+ }
+
+ const rolePermissions =
+ identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
+ const additionalPrivileges =
+ identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
+ role: ProjectMembershipRole.Custom,
+ permissions
+ })) || [];
+
return {
- permission: buildProjectPermission(identityProjectPermission.roles),
+ permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
membership: identityProjectPermission,
hasRole: (role: string) =>
identityProjectPermission.roles.findIndex(
@@ -203,14 +256,32 @@ export const permissionServiceFactory = ({
};
};
- const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => {
+ const getServiceTokenProjectPermission = async (
+ serviceTokenId: string,
+ projectId: string,
+ actorOrgId: string | undefined
+ ) => {
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
if (!serviceToken) throw new BadRequestError({ message: "Service token not found" });
+ const serviceTokenProject = await projectDAL.findById(serviceToken.projectId);
+
+ if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" });
+
+ if (serviceTokenProject.orgId !== actorOrgId) {
+ throw new UnauthorizedError({ message: "Service token not a part of this organization" });
+ }
+
if (serviceToken.projectId !== projectId)
throw new UnauthorizedError({
message: "Failed to find service authorization for given project"
});
+
+ if (serviceTokenProject.orgId !== actorOrgId)
+ throw new UnauthorizedError({
+ message: "Failed to find service authorization for given project"
+ });
+
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
@@ -238,15 +309,16 @@ export const permissionServiceFactory = ({
type: T,
id: string,
projectId: string,
- actorOrgId?: string
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
): Promise> => {
switch (type) {
case ActorType.USER:
- return getUserProjectPermission(id, projectId, actorOrgId) as Promise>;
+ return getUserProjectPermission(id, projectId, actorAuthMethod, actorOrgId) as Promise>;
case ActorType.SERVICE:
- return getServiceTokenProjectPermission(id, projectId) as Promise>;
+ return getServiceTokenProjectPermission(id, projectId, actorOrgId) as Promise>;
case ActorType.IDENTITY:
- return getIdentityProjectPermission(id, projectId) as Promise>;
+ return getIdentityProjectPermission(id, projectId, actorOrgId) as Promise>;
default:
throw new UnauthorizedError({
message: "Permission not defined",
diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts
index 46dbdcc3b..b24024bd4 100644
--- a/backend/src/ee/services/permission/project-permission.ts
+++ b/backend/src/ee/services/permission/project-permission.ts
@@ -12,6 +12,7 @@ export enum ProjectPermissionActions {
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
+ Groups = "groups",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
@@ -41,6 +42,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
+ | [ProjectPermissionActions, ProjectPermissionSub.Groups]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
@@ -82,6 +84,11 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
+ can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
+ can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
+ can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
+
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
@@ -157,6 +164,8 @@ const buildMemberPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
+
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
@@ -209,6 +218,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts
new file mode 100644
index 000000000..6c15d2d5d
--- /dev/null
+++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-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 TProjectUserAdditionalPrivilegeDALFactory = ReturnType;
+
+export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => {
+ const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege);
+ return orm;
+};
diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts
new file mode 100644
index 000000000..c9ff2c7e0
--- /dev/null
+++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts
@@ -0,0 +1,212 @@
+import { ForbiddenError } from "@casl/ability";
+import ms from "ms";
+
+import { BadRequestError } from "@app/lib/errors";
+import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
+
+import { TPermissionServiceFactory } from "../permission/permission-service";
+import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
+import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
+import {
+ ProjectUserAdditionalPrivilegeTemporaryMode,
+ TCreateUserPrivilegeDTO,
+ TDeleteUserPrivilegeDTO,
+ TGetUserPrivilegeDetailsDTO,
+ TListUserPrivilegesDTO,
+ TUpdateUserPrivilegeDTO
+} from "./project-user-additional-privilege-types";
+
+type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
+ projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
+ projectMembershipDAL: Pick;
+ permissionService: Pick;
+};
+
+export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
+ typeof projectUserAdditionalPrivilegeServiceFactory
+>;
+
+export const projectUserAdditionalPrivilegeServiceFactory = ({
+ projectUserAdditionalPrivilegeDAL,
+ projectMembershipDAL,
+ permissionService
+}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => {
+ const create = async ({
+ slug,
+ actor,
+ actorId,
+ permissions: customPermission,
+ actorOrgId,
+ actorAuthMethod,
+ projectMembershipId,
+ ...dto
+ }: TCreateUserPrivilegeDTO) => {
+ const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
+ if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
+
+ const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId });
+ if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
+
+ if (!dto.isTemporary) {
+ const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
+ projectMembershipId,
+ slug,
+ permissions: customPermission
+ });
+ return additionalPrivilege;
+ }
+
+ const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
+ const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
+ projectMembershipId,
+ slug,
+ permissions: customPermission,
+ isTemporary: true,
+ temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
+ temporaryRange: dto.temporaryRange,
+ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
+ temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
+ });
+ return additionalPrivilege;
+ };
+
+ const updateById = async ({
+ privilegeId,
+ actorOrgId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ ...dto
+ }: TUpdateUserPrivilegeDTO) => {
+ const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
+ if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
+
+ const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
+ if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
+
+ if (dto?.slug) {
+ const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
+ slug: dto.slug,
+ projectMembershipId: projectMembership.id
+ });
+ if (existingSlug && existingSlug.id !== userPrivilege.id)
+ throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
+ }
+
+ const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
+ if (isTemporary) {
+ const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
+ const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
+ const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
+ ...dto,
+ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
+ temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
+ });
+ return additionalPrivilege;
+ }
+
+ const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
+ ...dto,
+ isTemporary: false,
+ temporaryAccessStartTime: null,
+ temporaryAccessEndTime: null,
+ temporaryRange: null,
+ temporaryMode: null
+ });
+ return additionalPrivilege;
+ };
+
+ const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
+ const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
+ if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
+
+ const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
+ if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
+
+ const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
+ return deletedPrivilege;
+ };
+
+ const getPrivilegeDetailsById = async ({
+ privilegeId,
+ actorOrgId,
+ actor,
+ actorId,
+ actorAuthMethod
+ }: TGetUserPrivilegeDetailsDTO) => {
+ const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
+ if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
+
+ const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
+ if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
+
+ return userPrivilege;
+ };
+
+ const listPrivileges = async ({
+ projectMembershipId,
+ actorOrgId,
+ actor,
+ actorId,
+ actorAuthMethod
+ }: TListUserPrivilegesDTO) => {
+ const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
+ if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectMembership.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
+
+ const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({ projectMembershipId });
+ return userPrivileges;
+ };
+
+ return {
+ create,
+ updateById,
+ deleteById,
+ getPrivilegeDetailsById,
+ listPrivileges
+ };
+};
diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts
new file mode 100644
index 000000000..572474270
--- /dev/null
+++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts
@@ -0,0 +1,40 @@
+import { TProjectPermission } from "@app/lib/types";
+
+export enum ProjectUserAdditionalPrivilegeTemporaryMode {
+ Relative = "relative"
+}
+
+export type TCreateUserPrivilegeDTO = (
+ | {
+ permissions: unknown;
+ projectMembershipId: string;
+ slug: string;
+ isTemporary: false;
+ }
+ | {
+ permissions: unknown;
+ projectMembershipId: string;
+ slug: string;
+ isTemporary: true;
+ temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ }
+) &
+ Omit;
+
+export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit &
+ Partial<{
+ permissions: unknown;
+ slug: string;
+ isTemporary: boolean;
+ temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ }>;
+
+export type TDeleteUserPrivilegeDTO = Omit & { privilegeId: string };
+
+export type TGetUserPrivilegeDetailsDTO = Omit & { privilegeId: string };
+
+export type TListUserPrivilegesDTO = Omit & { projectMembershipId: string };
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 e9249d4aa..f88182e61 100644
--- a/backend/src/ee/services/saml-config/saml-config-service.ts
+++ b/backend/src/ee/services/saml-config/saml-config-service.ts
@@ -55,6 +55,7 @@ export const samlConfigServiceFactory = ({
const createSamlCfg = async ({
cert,
actor,
+ actorAuthMethod,
actorOrgId,
orgId,
issuer,
@@ -63,7 +64,7 @@ export const samlConfigServiceFactory = ({
entryPoint,
authProvider
}: TCreateSamlCfgDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
const plan = await licenseService.getPlan(orgId);
@@ -146,6 +147,7 @@ export const samlConfigServiceFactory = ({
orgId,
actor,
actorOrgId,
+ actorAuthMethod,
cert,
actorId,
issuer,
@@ -153,7 +155,7 @@ export const samlConfigServiceFactory = ({
entryPoint,
authProvider
}: TUpdateSamlCfgDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
const plan = await licenseService.getPlan(orgId);
if (!plan.samlSSO)
@@ -238,6 +240,7 @@ export const samlConfigServiceFactory = ({
dto.actor,
dto.actorId,
ssoConfig.orgId,
+ dto.actorAuthMethod,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
@@ -316,6 +319,11 @@ export const samlConfigServiceFactory = ({
const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" });
+ // TODO(dangtony98): remove this after aliases update
+ if (authProvider === AuthMethod.KEYCLOAK_SAML && appCfg.LICENSE_SERVER_KEY) {
+ throw new BadRequestError({ message: "Keycloak SAML is not yet available on Infisical Cloud" });
+ }
+
if (user) {
await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership(
diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts
index ec7c066fc..df7694920 100644
--- a/backend/src/ee/services/saml-config/saml-config-types.ts
+++ b/backend/src/ee/services/saml-config/saml-config-types.ts
@@ -1,11 +1,12 @@
import { TOrgPermission } from "@app/lib/types";
-import { ActorType } from "@app/services/auth/auth-type";
+import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export enum SamlProviders {
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
- GOOGLE_SAML = "google-saml"
+ GOOGLE_SAML = "google-saml",
+ KEYCLOAK_SAML = "keycloak-saml"
}
export type TCreateSamlCfgDTO = {
@@ -26,7 +27,14 @@ export type TUpdateSamlCfgDTO = Partial<{
TOrgPermission;
export type TGetSamlCfgDTO =
- | { type: "org"; orgId: string; actor: ActorType; actorId: string; actorOrgId?: string }
+ | {
+ type: "org";
+ orgId: string;
+ actor: ActorType;
+ actorId: string;
+ actorAuthMethod: ActorAuthMethod;
+ actorOrgId: string | undefined;
+ }
| {
type: "orgSlug";
orgSlug: string;
diff --git a/backend/src/ee/services/scim/scim-fns.ts b/backend/src/ee/services/scim/scim-fns.ts
index 8b68870da..e816cffcf 100644
--- a/backend/src/ee/services/scim/scim-fns.ts
+++ b/backend/src/ee/services/scim/scim-fns.ts
@@ -1,4 +1,4 @@
-import { TListScimUsers, TScimUser } from "./scim-types";
+import { TListScimGroups, TListScimUsers, TScimGroup, TScimUser } from "./scim-types";
export const buildScimUserList = ({
scimUsers,
@@ -62,3 +62,47 @@ export const buildScimUser = ({
return scimUser;
};
+
+export const buildScimGroupList = ({
+ scimGroups,
+ offset,
+ limit
+}: {
+ scimGroups: TScimGroup[];
+ offset: number;
+ limit: number;
+}): TListScimGroups => {
+ return {
+ Resources: scimGroups,
+ itemsPerPage: limit,
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
+ startIndex: offset,
+ totalResults: scimGroups.length
+ };
+};
+
+export const buildScimGroup = ({
+ groupId,
+ name,
+ members
+}: {
+ groupId: string;
+ name: string;
+ members: {
+ value: string;
+ display: string;
+ }[];
+}): TScimGroup => {
+ const scimGroup = {
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
+ id: groupId,
+ displayName: name,
+ members,
+ meta: {
+ resourceType: "Group",
+ location: null
+ }
+ };
+
+ return scimGroup;
+};
diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts
index c542b2340..15ca67a10 100644
--- a/backend/src/ee/services/scim/scim-service.ts
+++ b/backend/src/ee/services/scim/scim-service.ts
@@ -1,10 +1,13 @@
import { ForbiddenError } from "@casl/ability";
+import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
-import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
+import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas";
+import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
+import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -17,16 +20,23 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
-import { buildScimUser, buildScimUserList } from "./scim-fns";
+import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList } from "./scim-fns";
import {
+ TCreateScimGroupDTO,
TCreateScimTokenDTO,
TCreateScimUserDTO,
+ TDeleteScimGroupDTO,
TDeleteScimTokenDTO,
+ TDeleteScimUserDTO,
+ TGetScimGroupDTO,
TGetScimUserDTO,
+ TListScimGroupsDTO,
TListScimUsers,
TListScimUsersDTO,
TReplaceScimUserDTO,
TScimTokenJwtPayload,
+ TUpdateScimGroupNamePatchDTO,
+ TUpdateScimGroupNamePutDTO,
TUpdateScimUserDTO
} from "./scim-types";
@@ -39,6 +49,7 @@ type TScimServiceFactoryDep = {
>;
projectDAL: Pick;
projectMembershipDAL: Pick;
+ groupDAL: Pick;
licenseService: Pick;
permissionService: Pick;
smtpService: TSmtpService;
@@ -53,11 +64,20 @@ export const scimServiceFactory = ({
orgDAL,
projectDAL,
projectMembershipDAL,
+ groupDAL,
permissionService,
smtpService
}: TScimServiceFactoryDep) => {
- const createScimToken = async ({ actor, actorId, actorOrgId, orgId, description, ttlDays }: TCreateScimTokenDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const createScimToken = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ orgId,
+ description,
+ ttlDays
+ }: TCreateScimTokenDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(orgId);
@@ -85,8 +105,8 @@ export const scimServiceFactory = ({
return { scimToken };
};
- const listScimTokens = async ({ actor, actorId, actorOrgId, orgId }: TOrgPermission) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const listScimTokens = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TOrgPermission) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(orgId);
@@ -99,11 +119,17 @@ export const scimServiceFactory = ({
return scimTokens;
};
- const deleteScimToken = async ({ scimTokenId, actor, actorId, actorOrgId }: TDeleteScimTokenDTO) => {
+ const deleteScimToken = async ({ scimTokenId, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteScimTokenDTO) => {
let scimToken = await scimDAL.findById(scimTokenId);
if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" });
- const { permission } = await permissionService.getOrgPermission(actor, actorId, scimToken.orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ scimToken.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(scimToken.orgId);
@@ -409,6 +435,221 @@ export const scimServiceFactory = ({
});
};
+ const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => {
+ const [membership] = await orgDAL
+ .findMembership({
+ userId,
+ [`${TableName.OrgMembership}.orgId` as "id"]: orgId
+ })
+ .catch(() => {
+ throw new ScimRequestError({
+ detail: "User not found",
+ status: 404
+ });
+ });
+
+ if (!membership)
+ throw new ScimRequestError({
+ detail: "User not found",
+ status: 404
+ });
+
+ if (!membership.scimEnabled) {
+ throw new ScimRequestError({
+ detail: "SCIM is disabled for the organization",
+ status: 403
+ });
+ }
+
+ await deleteOrgMembership({
+ orgMembershipId: membership.id,
+ orgId: membership.orgId,
+ orgDAL,
+ projectDAL,
+ projectMembershipDAL
+ });
+
+ return {}; // intentionally return empty object upon success
+ };
+
+ const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
+ const org = await orgDAL.findById(orgId);
+
+ if (!org.scimEnabled)
+ throw new ScimRequestError({
+ detail: "SCIM is disabled for the organization",
+ status: 403
+ });
+
+ const groups = await groupDAL.findGroups({
+ orgId
+ });
+
+ const scimGroups = groups.map((group) =>
+ buildScimGroup({
+ groupId: group.id,
+ name: group.name,
+ members: []
+ })
+ );
+
+ return buildScimGroupList({
+ scimGroups,
+ offset,
+ limit
+ });
+ };
+
+ const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => {
+ const org = await orgDAL.findById(orgId);
+
+ if (!org.scimEnabled)
+ throw new ScimRequestError({
+ detail: "SCIM is disabled for the organization",
+ status: 403
+ });
+
+ const group = await groupDAL.create({
+ name: displayName,
+ slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
+ orgId,
+ role: OrgMembershipRole.NoAccess
+ });
+
+ return buildScimGroup({
+ groupId: group.id,
+ name: group.name,
+ members: []
+ });
+ };
+
+ const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
+ const group = await groupDAL.findOne({
+ id: groupId,
+ orgId
+ });
+
+ if (!group) {
+ throw new ScimRequestError({
+ detail: "Group Not Found",
+ status: 404
+ });
+ }
+
+ const users = await groupDAL.findAllGroupMembers({
+ orgId: group.orgId,
+ groupId: group.id
+ });
+
+ return buildScimGroup({
+ groupId: group.id,
+ name: group.name,
+ members: users
+ .filter((user) => user.isPartOfGroup)
+ .map((user) => ({
+ value: user.id,
+ display: `${user.firstName} ${user.lastName}`
+ }))
+ });
+ };
+
+ const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => {
+ const [group] = await groupDAL.update(
+ {
+ id: groupId,
+ orgId
+ },
+ {
+ name: displayName
+ }
+ );
+
+ if (!group) {
+ throw new ScimRequestError({
+ detail: "Group Not Found",
+ status: 404
+ });
+ }
+
+ return buildScimGroup({
+ groupId: group.id,
+ name: group.name,
+ members: []
+ });
+ };
+
+ // TODO: add support for add/remove op
+ const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
+ const org = await orgDAL.findById(orgId);
+
+ if (!org.scimEnabled)
+ throw new ScimRequestError({
+ detail: "SCIM is disabled for the organization",
+ status: 403
+ });
+
+ let group: TGroups | undefined;
+ for await (const operation of operations) {
+ switch (operation.op) {
+ case "replace": {
+ await groupDAL.update(
+ {
+ id: groupId,
+ orgId
+ },
+ {
+ name: operation.value.displayName
+ }
+ );
+ break;
+ }
+ case "add": {
+ // TODO
+ break;
+ }
+ case "remove": {
+ // TODO
+ break;
+ }
+ default: {
+ throw new ScimRequestError({
+ detail: "Invalid Operation",
+ status: 400
+ });
+ }
+ }
+ }
+
+ if (!group) {
+ throw new ScimRequestError({
+ detail: "Group Not Found",
+ status: 404
+ });
+ }
+
+ return buildScimGroup({
+ groupId: group.id,
+ name: group.name,
+ members: []
+ });
+ };
+
+ const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
+ const [group] = await groupDAL.delete({
+ id: groupId,
+ orgId
+ });
+
+ if (!group) {
+ throw new ScimRequestError({
+ detail: "Group Not Found",
+ status: 404
+ });
+ }
+
+ return {}; // intentionally return empty object upon success
+ };
+
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
const scimToken = await scimDAL.findById(token.scimTokenId);
if (!scimToken) throw new UnauthorizedError();
@@ -441,6 +682,13 @@ export const scimServiceFactory = ({
createScimUser,
updateScimUser,
replaceScimUser,
+ deleteScimUser,
+ listScimGroups,
+ createScimGroup,
+ getScimGroup,
+ deleteScimGroup,
+ updateScimGroupNamePut,
+ updateScimGroupNamePatch,
fnValidateScimToken
};
};
diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts
index c99dec794..fc5df0b2e 100644
--- a/backend/src/ee/services/scim/scim-types.ts
+++ b/backend/src/ee/services/scim/scim-types.ts
@@ -59,6 +59,73 @@ export type TReplaceScimUserDTO = {
orgId: string;
};
+export type TDeleteScimUserDTO = {
+ userId: string;
+ orgId: string;
+};
+
+export type TListScimGroupsDTO = {
+ offset: number;
+ limit: number;
+ orgId: string;
+};
+
+export type TListScimGroups = {
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
+ totalResults: number;
+ Resources: TScimGroup[];
+ itemsPerPage: number;
+ startIndex: number;
+};
+
+export type TCreateScimGroupDTO = {
+ displayName: string;
+ orgId: string;
+};
+
+export type TGetScimGroupDTO = {
+ groupId: string;
+ orgId: string;
+};
+
+export type TUpdateScimGroupNamePutDTO = {
+ groupId: string;
+ orgId: string;
+ displayName: string;
+};
+
+export type TUpdateScimGroupNamePatchDTO = {
+ groupId: string;
+ orgId: string;
+ operations: (TRemoveOp | TReplaceOp | TAddOp)[];
+};
+
+type TReplaceOp = {
+ op: "replace";
+ value: {
+ id: string;
+ displayName: string;
+ };
+};
+
+type TRemoveOp = {
+ op: "remove";
+ path: string;
+};
+
+type TAddOp = {
+ op: "add";
+ value: {
+ value: string;
+ display?: string;
+ };
+};
+
+export type TDeleteScimGroupDTO = {
+ groupId: string;
+ orgId: string;
+};
+
export type TScimTokenJwtPayload = {
scimTokenId: string;
authTokenType: string;
@@ -86,3 +153,17 @@ export type TScimUser = {
location: null;
};
};
+
+export type TScimGroup = {
+ schemas: string[];
+ id: string;
+ displayName: string;
+ members: {
+ value: string;
+ display: string;
+ }[];
+ meta: {
+ resourceType: string;
+ location: null;
+ };
+};
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 9d65ec7cc..8ddadb9bf 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
@@ -45,6 +45,7 @@ export const secretApprovalPolicyServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
approvals,
approvers,
projectId,
@@ -54,7 +55,13 @@ export const secretApprovalPolicyServiceFactory = ({
if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@@ -98,6 +105,7 @@ export const secretApprovalPolicyServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
approvals,
secretPolicyId
}: TUpdateSapDTO) => {
@@ -108,6 +116,7 @@ export const secretApprovalPolicyServiceFactory = ({
actor,
actorId,
secretApprovalPolicy.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
@@ -152,7 +161,13 @@ export const secretApprovalPolicyServiceFactory = ({
};
};
- const deleteSecretApprovalPolicy = async ({ secretPolicyId, actor, actorId, actorOrgId }: TDeleteSapDTO) => {
+ const deleteSecretApprovalPolicy = async ({
+ secretPolicyId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TDeleteSapDTO) => {
const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
@@ -160,6 +175,7 @@ export const secretApprovalPolicyServiceFactory = ({
actor,
actorId,
sapPolicy.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
@@ -171,8 +187,20 @@ export const secretApprovalPolicyServiceFactory = ({
return sapPolicy;
};
- const getSecretApprovalPolicyByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TListSapDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getSecretApprovalPolicyByProjectId = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TListSapDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId });
@@ -201,10 +229,17 @@ export const secretApprovalPolicyServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
environment,
secretPath
}: TGetBoardSapDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
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 b48b6bf95..b8ad89e45 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
@@ -82,13 +82,14 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionDAL,
secretQueueService
}: TSecretApprovalRequestServiceFactoryDep) => {
- const requestCount = async ({ projectId, actor, actorId, actorOrgId }: TApprovalRequestCountDTO) => {
+ const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const { membership } = await permissionService.getProjectPermission(
actor as ActorType.USER,
actorId,
projectId,
+ actorAuthMethod,
actorOrgId
);
@@ -100,6 +101,7 @@ export const secretApprovalRequestServiceFactory = ({
projectId,
actorId,
actor,
+ actorAuthMethod,
actorOrgId,
status,
environment,
@@ -109,7 +111,13 @@ export const secretApprovalRequestServiceFactory = ({
}: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
- const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { membership } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
const approvals = await secretApprovalRequestDAL.findByProjectId({
projectId,
committer,
@@ -122,7 +130,13 @@ export const secretApprovalRequestServiceFactory = ({
return approvals;
};
- const getSecretApprovalDetails = async ({ actor, actorId, actorOrgId, id }: TSecretApprovalDetailsDTO) => {
+ const getSecretApprovalDetails = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ id
+ }: TSecretApprovalDetailsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
@@ -133,6 +147,7 @@ export const secretApprovalRequestServiceFactory = ({
actor,
actorId,
secretApprovalRequest.projectId,
+ actorAuthMethod,
actorOrgId
);
if (
@@ -150,7 +165,14 @@ export const secretApprovalRequestServiceFactory = ({
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets };
};
- const reviewApproval = async ({ approvalId, actor, status, actorId, actorOrgId }: TReviewRequestDTO) => {
+ const reviewApproval = async ({
+ approvalId,
+ actor,
+ status,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TReviewRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
@@ -160,6 +182,7 @@ export const secretApprovalRequestServiceFactory = ({
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
+ actorAuthMethod,
actorOrgId
);
if (
@@ -192,7 +215,14 @@ export const secretApprovalRequestServiceFactory = ({
return reviewStatus;
};
- const updateApprovalStatus = async ({ actorId, status, approvalId, actor, actorOrgId }: TStatusChangeDTO) => {
+ const updateApprovalStatus = async ({
+ actorId,
+ status,
+ approvalId,
+ actor,
+ actorOrgId,
+ actorAuthMethod
+ }: TStatusChangeDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
@@ -202,6 +232,7 @@ export const secretApprovalRequestServiceFactory = ({
ActorType.USER,
actorId,
secretApprovalRequest.projectId,
+ actorAuthMethod,
actorOrgId
);
if (
@@ -229,7 +260,8 @@ export const secretApprovalRequestServiceFactory = ({
approvalId,
actor,
actorId,
- actorOrgId
+ actorOrgId,
+ actorAuthMethod
}: TMergeSecretApprovalRequestDTO) => {
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
@@ -240,8 +272,10 @@ export const secretApprovalRequestServiceFactory = ({
ActorType.USER,
actorId,
projectId,
+ actorAuthMethod,
actorOrgId
);
+
if (
!hasRole(ProjectMembershipRole.Admin) &&
secretApprovalRequest.committerId !== membership.id &&
@@ -438,6 +472,7 @@ export const secretApprovalRequestServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
policy,
projectId,
secretPath,
@@ -449,6 +484,7 @@ export const secretApprovalRequestServiceFactory = ({
actor,
actorId,
projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts
index c67477bfd..8eade1626 100644
--- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts
+++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts
@@ -9,6 +9,7 @@ import jmespath from "jmespath";
import knex from "knex";
import { getConfig } from "@app/lib/config/env";
+import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
@@ -89,7 +90,17 @@ export const secretRotationDbFn = async ({
const appCfg = getConfig();
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
- if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host))
+ const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
+ if (
+ host === "localhost" ||
+ host === "127.0.0.1" ||
+ // database infisical uses
+ dbHost === host ||
+ // internal ips
+ host === "host.docker.internal" ||
+ host.match(/^10\.\d+\.\d+\.\d+/) ||
+ host.match(/^192\.168\.\d+\.\d+/)
+ )
throw new Error("Invalid db host");
const db = knex({
diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts
index 75c19c6e9..1e1648a66 100644
--- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts
+++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts
@@ -39,8 +39,20 @@ export const secretRotationServiceFactory = ({
folderDAL,
secretDAL
}: TSecretRotationServiceFactoryDep) => {
- const getProviderTemplates = async ({ actor, actorId, actorOrgId, projectId }: TProjectPermission) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getProviderTemplates = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TProjectPermission) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
return {
@@ -54,6 +66,7 @@ export const secretRotationServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
inputs,
outputs,
interval,
@@ -61,7 +74,13 @@ export const secretRotationServiceFactory = ({
secretPath,
environment
}: TCreateSecretRotationDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
@@ -139,14 +158,20 @@ export const secretRotationServiceFactory = ({
return secretRotation;
};
- const getByProjectId = async ({ actorId, projectId, actor, actorOrgId }: TListByProjectIdDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getByProjectId = async ({ actorId, projectId, actor, actorOrgId, actorAuthMethod }: TListByProjectIdDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
const doc = await secretRotationDAL.find({ projectId });
return doc;
};
- const restartById = async ({ actor, actorId, actorOrgId, rotationId }: TRestartDTO) => {
+ const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {
const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
@@ -157,18 +182,30 @@ export const secretRotationServiceFactory = ({
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
});
- const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ doc.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
await secretRotationQueue.addToQueue(doc.id, doc.interval);
return doc;
};
- const deleteById = async ({ actor, actorId, actorOrgId, rotationId }: TDeleteDTO) => {
+ const deleteById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TDeleteDTO) => {
const doc = await secretRotationDAL.findById(rotationId);
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ doc.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation
diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts
index 7066fd485..9b78da3af 100644
--- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts
+++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts
@@ -39,8 +39,14 @@ export const secretScanningServiceFactory = ({
permissionService,
secretScanningQueue
}: TSecretScanningServiceFactoryDep) => {
- const createInstallationSession = async ({ actor, orgId, actorId, actorOrgId }: TInstallAppSessionDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const createInstallationSession = async ({
+ actor,
+ orgId,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TInstallAppSessionDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const sessionId = crypto.randomBytes(16).toString("hex");
@@ -53,12 +59,19 @@ export const secretScanningServiceFactory = ({
actorId,
installationId,
actor,
+ actorAuthMethod,
actorOrgId
}: TLinkInstallSessionDTO) => {
const session = await gitAppInstallSessionDAL.findOne({ sessionId });
if (!session) throw new UnauthorizedError({ message: "Session not found" });
- const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ session.orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const installatedApp = await gitAppOrgDAL.transaction(async (tx) => {
await gitAppInstallSessionDAL.deleteById(session.id, tx);
@@ -89,23 +102,37 @@ export const secretScanningServiceFactory = ({
return { installatedApp };
};
- const getOrgInstallationStatus = async ({ actorId, orgId, actor, actorOrgId }: TGetOrgInstallStatusDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getOrgInstallationStatus = async ({
+ actorId,
+ orgId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TGetOrgInstallStatusDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const appInstallation = await gitAppOrgDAL.findOne({ orgId });
return Boolean(appInstallation);
};
- const getRisksByOrg = async ({ actor, orgId, actorId, actorOrgId }: TGetOrgRisksDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetOrgRisksDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
return { risks };
};
- const updateRiskStatus = async ({ actorId, orgId, actor, actorOrgId, riskId, status }: TUpdateRiskStatusDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const updateRiskStatus = async ({
+ actorId,
+ orgId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ riskId,
+ status
+ }: TUpdateRiskStatusDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
const isRiskResolved = Boolean(
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 6ec7a23d5..0e71ad126 100644
--- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
+++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
@@ -1,4 +1,4 @@
-import { ForbiddenError } from "@casl/ability";
+import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert } from "@app/db/schemas";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
@@ -23,6 +23,7 @@ import {
import { TSnapshotDALFactory } from "./snapshot-dal";
import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
+import { getFullFolderPath } from "./snapshot-service-fns";
type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory;
@@ -33,7 +34,7 @@ type TSecretSnapshotServiceFactoryDep = {
secretDAL: Pick;
secretTagDAL: Pick;
secretVersionTagDAL: Pick;
- folderDAL: Pick;
+ folderDAL: Pick;
permissionService: Pick;
licenseService: Pick;
};
@@ -59,11 +60,24 @@ export const secretSnapshotServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
path
}: TProjectSnapshotCountDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
+ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
+ );
+
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -77,13 +91,26 @@ export const secretSnapshotServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
path,
limit = 20,
offset = 0
}: TProjectSnapshotListDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
+ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
+ );
+
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -91,11 +118,30 @@ export const secretSnapshotServiceFactory = ({
return snapshots;
};
- const getSnapshotData = async ({ actorId, actor, actorOrgId, id }: TGetSnapshotDataDTO) => {
+ const getSnapshotData = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetSnapshotDataDTO) => {
const snapshot = await snapshotDAL.findSecretSnapshotDataById(id);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ snapshot.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
+
+ const fullFolderPath = await getFullFolderPath({
+ folderDAL,
+ folderId: snapshot.folderId,
+ envId: snapshot.environment.id
+ });
+
+ // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment: snapshot.environment.slug, secretPath: fullFolderPath })
+ );
+
return snapshot;
};
@@ -145,11 +191,23 @@ export const secretSnapshotServiceFactory = ({
}
};
- const rollbackSnapshot = async ({ id: snapshotId, actor, actorId, actorOrgId }: TRollbackSnapshotDTO) => {
+ const rollbackSnapshot = async ({
+ id: snapshotId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TRollbackSnapshotDTO) => {
const snapshot = await snapshotDAL.findById(snapshotId);
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ snapshot.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback
diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts
index 41524c6eb..cdd5a999b 100644
--- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts
+++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts
@@ -101,6 +101,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
key: "snapshotId",
parentMapper: ({
snapshotId: id,
+ folderId,
projectId,
envId,
envSlug,
@@ -109,6 +110,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
snapshotUpdatedAt: updatedAt
}) => ({
id,
+ folderId,
projectId,
createdAt,
updatedAt,
diff --git a/backend/src/ee/services/secret-snapshot/snapshot-service-fns.ts b/backend/src/ee/services/secret-snapshot/snapshot-service-fns.ts
new file mode 100644
index 000000000..51cb9c056
--- /dev/null
+++ b/backend/src/ee/services/secret-snapshot/snapshot-service-fns.ts
@@ -0,0 +1,28 @@
+import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
+
+type GetFullFolderPath = {
+ folderDAL: Pick; // Added findAllInEnv
+ folderId: string;
+ envId: string;
+};
+
+export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise => {
+ // Helper function to remove duplicate slashes
+ const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
+
+ // Fetch all folders at once based on environment ID to avoid multiple queries
+ const folders = await folderDAL.find({ envId });
+ const folderMap = new Map(folders.map((folder) => [folder.id, folder]));
+
+ const buildPath = (currFolderId: string): string => {
+ const folder = folderMap.get(currFolderId);
+ if (!folder) return "";
+ const folderPathSegment = !folder.parentId && folder.name === "root" ? "/" : `/${folder.name}`;
+ if (folder.parentId) {
+ return removeDuplicateSlashes(`${buildPath(folder.parentId)}${folderPathSegment}`);
+ }
+ return removeDuplicateSlashes(folderPathSegment);
+ };
+
+ return buildPath(folderId);
+};
diff --git a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts
index 14c73db1f..ecd2b3070 100644
--- a/backend/src/ee/services/trusted-ip/trusted-ip-service.ts
+++ b/backend/src/ee/services/trusted-ip/trusted-ip-service.ts
@@ -26,8 +26,14 @@ export const trustedIpServiceFactory = ({
licenseService,
projectDAL
}: TTrustedIpServiceFactoryDep) => {
- const listIpsByProjectId = async ({ projectId, actor, actorId, actorOrgId }: TProjectPermission) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const listIpsByProjectId = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TProjectPermission) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
const trustedIps = await trustedIpDAL.find({
projectId
@@ -38,13 +44,20 @@ export const trustedIpServiceFactory = ({
const addProjectIp = async ({
projectId,
actorId,
+ actorAuthMethod,
actor,
actorOrgId,
ipAddress: ip,
comment,
isActive
}: TCreateIpDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId);
@@ -78,11 +91,18 @@ export const trustedIpServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
ipAddress: ip,
comment,
trustedIpId
}: TUpdateIpDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId);
@@ -113,8 +133,21 @@ export const trustedIpServiceFactory = ({
return { trustedIp, project }; // for audit log
};
- const deleteProjectIp = async ({ projectId, actorId, actor, actorOrgId, trustedIpId }: TDeleteIpDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const deleteProjectIp = async ({
+ projectId,
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ trustedIpId
+ }: TDeleteIpDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
const project = await projectDAL.findById(projectId);
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
new file mode 100644
index 000000000..d890fcc35
--- /dev/null
+++ b/backend/src/lib/api-docs/constants.ts
@@ -0,0 +1,612 @@
+export const GROUPS = {
+ CREATE: {
+ name: "The name of the group to create.",
+ slug: "The slug of the group to create.",
+ role: "The role of the group to create."
+ },
+ UPDATE: {
+ currentSlug: "The current slug of the group to update.",
+ name: "The new name of the group to update to.",
+ slug: "The new slug of the group to update to.",
+ role: "The new role of the group to update to."
+ },
+ DELETE: {
+ slug: "The slug of the group to delete"
+ },
+ LIST_USERS: {
+ slug: "The slug of the group to list users for",
+ offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
+ limit: "The number of users to return.",
+ username: "The username to search for."
+ },
+ ADD_USER: {
+ slug: "The slug of the group to add the user to.",
+ username: "The username of the user to add to the group."
+ },
+ DELETE_USER: {
+ slug: "The slug of the group to remove the user from.",
+ username: "The username of the user to remove from the group."
+ }
+} as const;
+
+export const IDENTITIES = {
+ CREATE: {
+ name: "The name of the identity to create.",
+ organizationId: "The organization ID to which the identity belongs.",
+ role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'."
+ },
+ UPDATE: {
+ identityId: "The ID of the identity to update.",
+ name: "The new name of the identity.",
+ role: "The new role of the identity."
+ },
+ DELETE: {
+ identityId: "The ID of the identity to delete."
+ }
+} as const;
+
+export const UNIVERSAL_AUTH = {
+ LOGIN: {
+ clientId: "Your Machine Identity Client ID.",
+ clientSecret: "Your Machine Identity Client Secret."
+ },
+ ATTACH: {
+ identityId: "The ID of the identity to attach the configuration onto.",
+ clientSecretTrustedIps:
+ "A list of IPs or CIDR ranges that the Client Secret can be used from together with the Client ID to get back an access token. You can use 0.0.0.0/0, to allow usage from any network address.",
+ accessTokenTrustedIps:
+ "A list of IPs or CIDR ranges that access tokens can be used from. You can use 0.0.0.0/0, to allow usage from any network address.",
+ accessTokenTTL: "The lifetime for an access token in seconds. This value will be referenced at renewal time.",
+ accessTokenMaxTTL:
+ "The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.",
+ accessTokenNumUsesLimit:
+ "The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
+ },
+ RETRIEVE: {
+ identityId: "The ID of the identity to retrieve."
+ },
+ UPDATE: {
+ identityId: "The ID of the identity to update.",
+ clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.",
+ accessTokenTrustedIps: "The new list of IPs or CIDR ranges that access tokens can be used from.",
+ accessTokenTTL: "The new lifetime for an access token in seconds.",
+ accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
+ accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
+ },
+ CREATE_CLIENT_SECRET: {
+ identityId: "The ID of the identity to create a client secret for.",
+ description: "The description of the client secret.",
+ numUsesLimit:
+ "The maximum number of times that the client secret can be used; a value of 0 implies infinite number of uses.",
+ ttl: "The lifetime for the client secret in seconds."
+ },
+ LIST_CLIENT_SECRETS: {
+ identityId: "The ID of the identity to list client secrets for."
+ },
+ REVOKE_CLIENT_SECRET: {
+ identityId: "The ID of the identity to revoke the client secret from.",
+ clientSecretId: "The ID of the client secret to revoke."
+ },
+ RENEW_ACCESS_TOKEN: {
+ accessToken: "The access token to renew."
+ }
+} as const;
+
+export const ORGANIZATIONS = {
+ LIST_USER_MEMBERSHIPS: {
+ organizationId: "The ID of the organization to get memberships from."
+ },
+ UPDATE_USER_MEMBERSHIP: {
+ organizationId: "The ID of the organization to update the membership for.",
+ membershipId: "The ID of the membership to update.",
+ role: "The new role of the membership."
+ },
+ DELETE_USER_MEMBERSHIP: {
+ organizationId: "The ID of the organization to delete the membership from.",
+ membershipId: "The ID of the membership to delete."
+ },
+ LIST_IDENTITY_MEMBERSHIPS: {
+ orgId: "The ID of the organization to get identity memberships from."
+ },
+ GET_PROJECTS: {
+ organizationId: "The ID of the organization to get projects from."
+ },
+ LIST_GROUPS: {
+ organizationId: "The ID of the organization to list groups for."
+ }
+} as const;
+
+export const PROJECTS = {
+ CREATE: {
+ organizationSlug: "The slug of the organization to create the project in.",
+ projectName: "The name of the project to create.",
+ slug: "An optional slug for the project."
+ },
+ DELETE: {
+ workspaceId: "The ID of the project to delete."
+ },
+ GET: {
+ workspaceId: "The ID of the project."
+ },
+ UPDATE: {
+ workspaceId: "The ID of the project to update.",
+ name: "The new name of the project.",
+ autoCapitalization: "Disable or enable auto-capitalization for the project."
+ },
+ INVITE_MEMBER: {
+ projectId: "The ID of the project to invite the member to.",
+ emails: "A list of organization member emails to invite to the project.",
+ usernames: "A list of usernames to invite to the project."
+ },
+ REMOVE_MEMBER: {
+ projectId: "The ID of the project to remove the member from.",
+ emails: "A list of organization member emails to remove from the project.",
+ usernames: "A list of usernames to remove from the project."
+ },
+ GET_USER_MEMBERSHIPS: {
+ workspaceId: "The ID of the project to get memberships from."
+ },
+ UPDATE_USER_MEMBERSHIP: {
+ workspaceId: "The ID of the project to update the membership for.",
+ membershipId: "The ID of the membership to update.",
+ roles: "A list of roles to update the membership to."
+ },
+ LIST_IDENTITY_MEMBERSHIPS: {
+ projectId: "The ID of the project to get identity memberships from."
+ },
+ UPDATE_IDENTITY_MEMBERSHIP: {
+ projectId: "The ID of the project to update the identity membership for.",
+ identityId: "The ID of the identity to update the membership for.",
+ roles: "A list of roles to update the membership to."
+ },
+ DELETE_IDENTITY_MEMBERSHIP: {
+ projectId: "The ID of the project to delete the identity membership from.",
+ identityId: "The ID of the identity to delete the membership from."
+ },
+ GET_KEY: {
+ workspaceId: "The ID of the project to get the key from."
+ },
+ GET_SNAPSHOTS: {
+ workspaceId: "The ID of the project to get snapshots from.",
+ environment: "The environment to get snapshots from.",
+ path: "The secret path to get snapshots from.",
+ offset: "The offset to start from. If you enter 10, it will start from the 10th snapshot.",
+ limit: "The number of snapshots to return."
+ },
+ ROLLBACK_TO_SNAPSHOT: {
+ secretSnapshotId: "The ID of the snapshot to rollback to."
+ },
+ ADD_GROUP_TO_PROJECT: {
+ projectSlug: "The slug of the project to add the group to.",
+ groupSlug: "The slug of the group to add to the project.",
+ role: "The role for the group to assume in the project."
+ },
+ UPDATE_GROUP_IN_PROJECT: {
+ projectSlug: "The slug of the project to update the group in.",
+ groupSlug: "The slug of the group to update in the project.",
+ roles: "A list of roles to update the group to."
+ },
+ REMOVE_GROUP_FROM_PROJECT: {
+ projectSlug: "The slug of the project to delete the group from.",
+ groupSlug: "The slug of the group to delete from the project."
+ },
+ LIST_GROUPS_IN_PROJECT: {
+ projectSlug: "The slug of the project to list groups for."
+ },
+ LIST_INTEGRATION: {
+ workspaceId: "The ID of the project to list integrations for."
+ },
+ LIST_INTEGRATION_AUTHORIZATION: {
+ workspaceId: "The ID of the project to list integration auths for."
+ }
+} as const;
+
+export const ENVIRONMENTS = {
+ CREATE: {
+ workspaceId: "The ID of the project to create the environment in.",
+ name: "The name of the environment to create.",
+ slug: "The slug of the environment to create."
+ },
+ UPDATE: {
+ workspaceId: "The ID of the project to update the environment in.",
+ id: "The ID of the environment to update.",
+ name: "The new name of the environment.",
+ slug: "The new slug of the environment.",
+ position: "The new position of the environment. The lowest number will be displayed as the first environment."
+ },
+ DELETE: {
+ workspaceId: "The ID of the project to delete the environment from.",
+ id: "The ID of the environment to delete."
+ }
+} as const;
+
+export const FOLDERS = {
+ LIST: {
+ workspaceId: "The ID of the project to list folders from.",
+ environment: "The slug of the environment to list folders from.",
+ path: "The path to list folders from.",
+ directory: "The directory to list folders from. (Deprecated in favor of path)"
+ },
+ CREATE: {
+ workspaceId: "The ID of the project to create the folder in.",
+ environment: "The slug of the environment to create the folder in.",
+ name: "The name of the folder to create.",
+ path: "The path of the folder to create.",
+ directory: "The directory of the folder to create. (Deprecated in favor of path)"
+ },
+ UPDATE: {
+ folderId: "The ID of the folder to update.",
+ environment: "The slug of the environment where the folder is located.",
+ name: "The new name of the folder.",
+ path: "The path of the folder to update.",
+ directory: "The new directory of the folder to update. (Deprecated in favor of path)",
+ workspaceId: "The ID of the project where the folder is located."
+ },
+ DELETE: {
+ folderIdOrName: "The ID or name of the folder to delete.",
+ workspaceId: "The ID of the project to delete the folder from.",
+ environment: "The slug of the environment where the folder is located.",
+ directory: "The directory of the folder to delete. (Deprecated in favor of path)",
+ path: "The path of the folder to delete."
+ }
+} as const;
+
+export const SECRETS = {
+ ATTACH_TAGS: {
+ secretName: "The name of the secret to attach tags to.",
+ secretPath: "The path of the secret to attach tags to.",
+ type: "The type of the secret to attach tags to. (shared/personal)",
+ environment: "The slug of the environment where the secret is located",
+ projectSlug: "The slug of the project where the secret is located",
+ tagSlugs: "An array of existing tag slugs to attach to the secret."
+ },
+ DETACH_TAGS: {
+ secretName: "The name of the secret to detach tags from.",
+ secretPath: "The path of the secret to detach tags from.",
+ type: "The type of the secret to attach tags to. (shared/personal)",
+ environment: "The slug of the environment where the secret is located",
+ projectSlug: "The slug of the project where the secret is located",
+ tagSlugs: "An array of existing tag slugs to detach from the secret."
+ }
+} as const;
+
+export const RAW_SECRETS = {
+ LIST: {
+ recursive:
+ "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
+ workspaceId: "The ID of the project to list secrets from.",
+ workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
+ environment: "The slug of the environment to list secrets from.",
+ secretPath: "The secret path to list secrets from.",
+ includeImports: "Weather to include imported secrets or not."
+ },
+ CREATE: {
+ secretName: "The name of the secret to create.",
+ environment: "The slug of the environment to create the secret in.",
+ secretComment: "Attach a comment to the secret.",
+ secretPath: "The path to create the secret in.",
+ 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."
+ },
+ GET: {
+ secretName: "The name of the secret to get.",
+ workspaceId: "The ID of the project to get the secret from.",
+ environment: "The slug of the environment to get the secret from.",
+ secretPath: "The path of the secret to get.",
+ version: "The version of the secret to get.",
+ type: "The type of the secret to get.",
+ includeImports: "Weather to include imported secrets or not."
+ },
+ UPDATE: {
+ secretName: "The name of the secret to update.",
+ environment: "The slug of the environment where the secret is located.",
+ secretPath: "The path of the secret to update",
+ secretValue: "The new value of the secret.",
+ skipMultilineEncoding: "Skip multiline encoding for the secret value.",
+ type: "The type of the secret to update.",
+ workspaceId: "The ID of the project to update the secret in."
+ },
+ DELETE: {
+ secretName: "The name of the secret to delete.",
+ environment: "The slug of the environment where the secret is located.",
+ secretPath: "The path of the secret.",
+ type: "The type of the secret to delete.",
+ workspaceId: "The ID of the project where the secret is located."
+ }
+} as const;
+
+export const SECRET_IMPORTS = {
+ LIST: {
+ workspaceId: "The ID of the project to list secret imports from.",
+ environment: "The slug of the environment to list secret imports from.",
+ path: "The path to list secret imports from."
+ },
+ CREATE: {
+ 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.",
+ import: {
+ environment: "The slug of the environment to import from.",
+ path: "The path to import from."
+ }
+ },
+ UPDATE: {
+ secretImportId: "The ID of the secret import to update.",
+ environment: "The slug of the environment where the secret import is located.",
+ import: {
+ environment: "The new environment slug to import from.",
+ path: "The new path to import from.",
+ position: "The new position of the secret import. The lowest number will be displayed as the first import."
+ },
+ path: "The path of the secret import to update.",
+ workspaceId: "The ID of the project where the secret import is located."
+ },
+ DELETE: {
+ workspaceId: "The ID of the project to delete the secret import from.",
+ secretImportId: "The ID of the secret import to delete.",
+ environment: "The slug of the environment where the secret import is located.",
+ path: "The path of the secret import to delete."
+ }
+} as const;
+
+export const AUDIT_LOGS = {
+ EXPORT: {
+ workspaceId: "The ID of the project to export audit logs from.",
+ eventType: "The type of the event to export.",
+ userAgentType: "Choose which consuming application to export audit logs for.",
+ startDate: "The date to start the export from.",
+ endDate: "The date to end the export at.",
+ offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
+ limit: "The number of audit logs to return.",
+ actor: "The actor to filter the audit logs by."
+ }
+} as const;
+
+export const DYNAMIC_SECRETS = {
+ LIST: {
+ projectSlug: "The slug of the project to create dynamic secret in.",
+ environmentSlug: "The slug of the environment to list folders from.",
+ path: "The path to list folders from."
+ },
+ LIST_LEAES_BY_NAME: {
+ projectSlug: "The slug of the project to create dynamic secret in.",
+ environmentSlug: "The slug of the environment to list folders from.",
+ path: "The path to list folders from.",
+ name: "The name of the dynamic secret."
+ },
+ GET_BY_NAME: {
+ projectSlug: "The slug of the project to create dynamic secret in.",
+ environmentSlug: "The slug of the environment to list folders from.",
+ path: "The path to list folders from.",
+ name: "The name of the dynamic secret."
+ },
+ CREATE: {
+ projectSlug: "The slug of the project to create dynamic secret in.",
+ environmentSlug: "The slug of the environment to create the dynamic secret in.",
+ path: "The path to create the dynamic secret in.",
+ name: "The name of the dynamic secret.",
+ provider: "The type of dynamic secret.",
+ defaultTTL: "The default TTL that will be applied for all the leases.",
+ maxTTL: "The maximum limit a TTL can be leases or renewed."
+ },
+ UPDATE: {
+ projectSlug: "The slug of the project to update dynamic secret in.",
+ environmentSlug: "The slug of the environment to update the dynamic secret in.",
+ path: "The path to update the dynamic secret in.",
+ name: "The name of the dynamic secret.",
+ inputs: "The new partial values for the configurated provider of the dynamic secret",
+ defaultTTL: "The default TTL that will be applied for all the leases.",
+ maxTTL: "The maximum limit a TTL can be leases or renewed.",
+ newName: "The new name for the dynamic secret."
+ },
+ DELETE: {
+ projectSlug: "The slug of the project to delete dynamic secret in.",
+ environmentSlug: "The slug of the environment to delete the dynamic secret in.",
+ path: "The path to delete the dynamic secret in.",
+ name: "The name of the dynamic secret.",
+ isForced:
+ "A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
+ }
+} as const;
+
+export const DYNAMIC_SECRET_LEASES = {
+ GET_BY_LEASEID: {
+ projectSlug: "The slug of the project to create dynamic secret in.",
+ environmentSlug: "The slug of the environment to list folders from.",
+ path: "The path to list folders from.",
+ leaseId: "The ID of the dynamic secret lease."
+ },
+ CREATE: {
+ projectSlug: "The slug of the project of the dynamic secret in.",
+ environmentSlug: "The slug of the environment of the dynamic secret in.",
+ path: "The path of the dynamic secret in.",
+ dynamicSecretName: "The name of the dynamic secret.",
+ ttl: "The lease lifetime ttl. If not provided the default TTL of dynamic secret will be used."
+ },
+ RENEW: {
+ projectSlug: "The slug of the project of the dynamic secret in.",
+ environmentSlug: "The slug of the environment of the dynamic secret in.",
+ path: "The path of the dynamic secret in.",
+ leaseId: "The ID of the dynamic secret lease.",
+ ttl: "The renew TTL that gets added with current expiry (ensure it's below max TTL) for a total less than creation time + max TTL."
+ },
+ DELETE: {
+ projectSlug: "The slug of the project of the dynamic secret in.",
+ environmentSlug: "The slug of the environment of the dynamic secret in.",
+ path: "The path of the dynamic secret in.",
+ leaseId: "The ID of the dynamic secret lease.",
+ isForced:
+ "A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
+ }
+} as const;
+export const SECRET_TAGS = {
+ LIST: {
+ projectId: "The ID of the project to list tags from."
+ },
+ 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."
+ },
+ DELETE: {
+ tagId: "The ID of the tag to delete.",
+ projectId: "The ID of the project to delete the tag from."
+ }
+} as const;
+
+export const IDENTITY_ADDITIONAL_PRIVILEGE = {
+ CREATE: {
+ projectSlug: "The slug of the project of the identity in.",
+ identityId: "The ID of the identity to delete.",
+ slug: "The slug of the privilege to create.",
+ permissions: `The permission object for the privilege.
+1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
+2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
+2. [["read", "secrets", {environment: "dev"}]]
+`,
+ isPackPermission: "Whether the server should pack(compact) the permission object.",
+ isTemporary: "Whether the privilege is temporary.",
+ temporaryMode: "Type of temporary access given. Types: relative",
+ temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
+ temporaryAccessStartTime: "ISO time for which temporary access should begin."
+ },
+ UPDATE: {
+ projectSlug: "The slug of the project of the identity in.",
+ 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.
+1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
+2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
+2. [["read", "secrets", {environment: "dev"}]]
+`,
+ isPackPermission: "Whether the server should pack(compact) the permission object.",
+ isTemporary: "Whether the privilege is temporary.",
+ temporaryMode: "Type of temporary access given. Types: relative",
+ temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
+ temporaryAccessStartTime: "ISO time for which temporary access should begin."
+ },
+ DELETE: {
+ projectSlug: "The slug of the project of the identity in.",
+ identityId: "The ID of the identity to delete.",
+ slug: "The slug of the privilege to delete."
+ },
+ GET_BY_SLUG: {
+ projectSlug: "The slug of the project of the identity in.",
+ identityId: "The ID of the identity to list.",
+ slug: "The slug of the privilege."
+ },
+ LIST: {
+ projectSlug: "The slug of the project of the identity in.",
+ identityId: "The ID of the identity to list.",
+ unpacked: "Whether the system should send the permissions as unpacked"
+ }
+};
+
+export const PROJECT_USER_ADDITIONAL_PRIVILEGE = {
+ CREATE: {
+ projectMembershipId: "Project membership id of user",
+ slug: "The slug of the privilege to create.",
+ permissions:
+ "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
+ isPackPermission: "Whether the server should pack(compact) the permission object.",
+ isTemporary: "Whether the privilege is temporary.",
+ temporaryMode: "Type of temporary access given. Types: relative",
+ temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
+ temporaryAccessStartTime: "ISO time for which temporary access should begin."
+ },
+ UPDATE: {
+ privilegeId: "The id of privilege object",
+ slug: "The slug of the privilege to create.",
+ newSlug: "The new slug of the privilege to create.",
+ permissions:
+ "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape",
+ isPackPermission: "Whether the server should pack(compact) the permission object.",
+ isTemporary: "Whether the privilege is temporary.",
+ temporaryMode: "Type of temporary access given. Types: relative",
+ temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
+ temporaryAccessStartTime: "ISO time for which temporary access should begin."
+ },
+ DELETE: {
+ privilegeId: "The id of privilege object"
+ },
+ GET_BY_PRIVILEGEID: {
+ privilegeId: "The id of privilege object"
+ },
+ LIST: {
+ projectMembershipId: "Project membership id of user"
+ }
+};
+
+export const INTEGRATION_AUTH = {
+ GET: {
+ integrationAuthId: "The id of integration authentication object."
+ },
+ DELETE: {
+ integration: "The slug of the integration to be unauthorized.",
+ projectId: "The ID of the project to delete the integration auth from."
+ },
+ DELETE_BY_ID: {
+ integrationAuthId: "The id of integration authentication object to delete."
+ },
+ CREATE_ACCESS_TOKEN: {
+ workspaceId: "The ID of the project to create the integration auth for.",
+ integration: "The slug of integration for the auth object.",
+ accessId: "The unique authorized access id of the external integration provider.",
+ accessToken: "The unique authorized access token of the external integration provider.",
+ url: "",
+ namespace: "",
+ refreshToken: "The refresh token for integration authorization."
+ }
+} as const;
+
+export const INTEGRATION = {
+ CREATE: {
+ integrationAuthId: "The ID of the integration auth object to link with integration.",
+ app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
+ isActive: "Whether the integration should be active or disabled.",
+ appId:
+ "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
+ secretPath: "The path of the secrets to sync secrets from.",
+ sourceEnvironment: "The environment to sync secret from.",
+ targetEnvironment:
+ "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
+ targetEnvironmentId:
+ "The target environment id of the integration provider. Used in cloudflare pages, teamcity, gitlab integrations.",
+ targetService:
+ "The service based grouping identifier of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
+ 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.",
+ 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",
+ metadata: {
+ secretPrefix: "The prefix for the saved secret. Used by GCP.",
+ secretSuffix: "The suffix for the saved secret. Used by GCP.",
+ initialSyncBehavoir: "Type of syncing behavoir with the integration.",
+ shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
+ secretGCPLabel: "The label for GCP secrets.",
+ secretAWSTag: "The tags for AWS secrets.",
+ kmsKeyId: "The ID of the encryption key from AWS KMS."
+ }
+ },
+ UPDATE: {
+ integrationId: "The ID of the integration object.",
+ app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
+ appId:
+ "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
+ isActive: "Whether the integration should be active or disabled.",
+ secretPath: "The path of the secrets to sync secrets from.",
+ owner: "External integration providers service entity owner. Used in Github.",
+ targetEnvironment:
+ "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
+ environment: "The environment to sync secrets from."
+ },
+ DELETE: {
+ integrationId: "The ID of the integration object."
+ }
+};
diff --git a/backend/src/lib/api-docs/index.ts b/backend/src/lib/api-docs/index.ts
new file mode 100644
index 000000000..b04bfcf75
--- /dev/null
+++ b/backend/src/lib/api-docs/index.ts
@@ -0,0 +1 @@
+export * from "./constants";
diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts
index 84d772e2e..4d3d55ffd 100644
--- a/backend/src/lib/config/env.ts
+++ b/backend/src/lib/config/env.ts
@@ -18,6 +18,7 @@ const envSchema = z
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
),
+ MAX_LEASE_LIMIT: z.coerce.number().default(10000),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"),
@@ -106,13 +107,15 @@ const envSchema = z
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
LICENSE_KEY: zpStr(z.string().optional()),
+ LICENSE_KEY_OFFLINE: zpStr(z.string().optional()),
// GENERIC
STANDALONE_MODE: z
.enum(["true", "false"])
.transform((val) => val === "true")
.optional(),
- INFISICAL_CLOUD: zodStrBool.default("false")
+ INFISICAL_CLOUD: zodStrBool.default("false"),
+ MAINTENANCE_MODE: zodStrBool.default("false")
})
.transform((data) => ({
...data,
diff --git a/backend/src/lib/crypto/index.ts b/backend/src/lib/crypto/index.ts
index 9d7f4e886..db3d91fc8 100644
--- a/backend/src/lib/crypto/index.ts
+++ b/backend/src/lib/crypto/index.ts
@@ -17,4 +17,5 @@ export {
decryptSecrets,
decryptSecretVersions
} from "./secret-encryption";
+export { verifyOfflineLicense } from "./signing";
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
diff --git a/backend/src/lib/crypto/license_public_key.pem b/backend/src/lib/crypto/license_public_key.pem
new file mode 100644
index 000000000..0cda06f3c
--- /dev/null
+++ b/backend/src/lib/crypto/license_public_key.pem
@@ -0,0 +1,8 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIIBCgKCAQEApchBY3BXTu4zWGBguB7nM/pjpVLY3V7VGZOAxmR5ueQTJOwiGM13
+5HN3EM9fDlQnZu9VSc0OFqRM/bUeUaI1oLPE6WzTHjdHyKjDI/S+TLx3VGEsvhM1
+uukZpYX+3KX2w4wzRHBaBWyglFy0CVNth9UJhhpD+KKfv7dzcRmsbyoUWi9wGfJu
+wLYCwaCwZRXIt1sLGmMncPz14vfwdnm2a5Tj1Jbt0GTyBl+1/ZqLbO6SsslLg2G+
+o7FfGS9z8OUTkvDdu16qxL+p2wCEFZMnOz5BB4oakuT2gS9iOO2l5AOPcT4WzPzy
+PYbX3d7cN9BkOY9I5z0cX4wzqHjQTvGNLQIDAQAB
+-----END RSA PUBLIC KEY-----
\ No newline at end of file
diff --git a/backend/src/lib/crypto/signing.ts b/backend/src/lib/crypto/signing.ts
new file mode 100644
index 000000000..36c858715
--- /dev/null
+++ b/backend/src/lib/crypto/signing.ts
@@ -0,0 +1,22 @@
+import crypto, { KeyObject } from "crypto";
+import fs from "fs/promises";
+import path from "path";
+
+export const verifySignature = (data: string, signature: Buffer, publicKey: KeyObject) => {
+ const verify = crypto.createVerify("SHA256");
+ verify.update(data);
+ verify.end();
+ return verify.verify(publicKey, signature);
+};
+
+export const verifyOfflineLicense = async (licenseContents: string, signature: string) => {
+ const publicKeyPem = await fs.readFile(path.join(__dirname, "license_public_key.pem"), "utf8");
+
+ const publicKey = crypto.createPublicKey({
+ key: publicKeyPem,
+ format: "pem",
+ type: "pkcs1"
+ });
+
+ return verifySignature(licenseContents, Buffer.from(signature, "base64"), publicKey);
+};
diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts
index d93244bbd..18b40acfd 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 DisableRotationErrors extends Error {
+ name: string;
+
+ error: unknown;
+
+ constructor({ name, error, message }: { message: string; name?: string; error?: unknown }) {
+ super(message);
+ this.name = name || "DisableRotationErrors";
+ this.error = error;
+ }
+}
+
export class ScimRequestError extends Error {
name: string;
diff --git a/backend/src/lib/fn/dates.ts b/backend/src/lib/fn/dates.ts
index e69de29bb..f9ea4db10 100644
--- a/backend/src/lib/fn/dates.ts
+++ b/backend/src/lib/fn/dates.ts
@@ -0,0 +1,2 @@
+export const getLastMidnightDateISO = (last = 1) =>
+ `${new Date(new Date().setDate(new Date().getDate() - last)).toISOString().slice(0, 10)}T00:00:00Z`;
diff --git a/backend/src/lib/fn/index.ts b/backend/src/lib/fn/index.ts
index 4b4a01a14..0d0f07e45 100644
--- a/backend/src/lib/fn/index.ts
+++ b/backend/src/lib/fn/index.ts
@@ -2,5 +2,6 @@
// Full credits goes to https://github.com/rayapps to those functions
// Code taken to keep in in house and to adjust somethings for our needs
export * from "./array";
+export * from "./dates";
export * from "./object";
export * from "./string";
diff --git a/backend/src/lib/knex/connection.ts b/backend/src/lib/knex/connection.ts
new file mode 100644
index 000000000..993615a0b
--- /dev/null
+++ b/backend/src/lib/knex/connection.ts
@@ -0,0 +1,11 @@
+import { URL } from "url"; // Import the URL class
+
+export const getDbConnectionHost = (urlString: string) => {
+ try {
+ const url = new URL(urlString);
+ // Split hostname and port (if provided)
+ return url.hostname.split(":")[0];
+ } catch (error) {
+ return null;
+ }
+};
diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts
index 37fae624e..d78020809 100644
--- a/backend/src/lib/knex/index.ts
+++ b/backend/src/lib/knex/index.ts
@@ -4,6 +4,7 @@ import { Tables } from "knex/types/tables";
import { DatabaseError } from "../errors";
+export * from "./connection";
export * from "./join";
export * from "./select";
diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts
index 918322d62..b3b46e739 100644
--- a/backend/src/lib/types/index.ts
+++ b/backend/src/lib/types/index.ts
@@ -1,17 +1,40 @@
-import { ActorType } from "@app/services/auth/auth-type";
+import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
+export type TGenericPermission = {
+ actor: ActorType;
+ actorId: string;
+ actorAuthMethod: ActorAuthMethod;
+ actorOrgId: string | undefined;
+};
+
+/**
+ * TODO(dangtony98): ideally move service fns to use TGenericPermission
+ * because TOrgPermission [orgId] is not as relevant anymore with the
+ * introduction of organizationIds bound to all user tokens
+ */
export type TOrgPermission = {
actor: ActorType;
actorId: string;
orgId: string;
- actorOrgId?: string;
+ actorAuthMethod: ActorAuthMethod;
+ actorOrgId: string | undefined;
};
export type TProjectPermission = {
actor: ActorType;
actorId: string;
projectId: string;
- actorOrgId?: string;
+ actorAuthMethod: ActorAuthMethod;
+ actorOrgId: string;
+};
+
+// same as TProjectPermission but with projectSlug requirement instead of projectId
+export type TProjectSlugPermission = {
+ actor: ActorType;
+ actorId: string;
+ projectSlug: string;
+ actorAuthMethod: ActorAuthMethod;
+ actorOrgId: string;
};
export type RequiredKeys = {
diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts
index 45c135b77..e1149120d 100644
--- a/backend/src/queue/queue-service.ts
+++ b/backend/src/queue/queue-service.ts
@@ -18,7 +18,8 @@ export enum QueueName {
SecretWebhook = "secret-webhook",
SecretFullRepoScan = "secret-full-repo-scan",
SecretPushEventScan = "secret-push-event-scan",
- UpgradeProjectToGhost = "upgrade-project-to-ghost"
+ UpgradeProjectToGhost = "upgrade-project-to-ghost",
+ DynamicSecretRevocation = "dynamic-secret-revocation"
}
export enum QueueJobs {
@@ -30,7 +31,9 @@ export enum QueueJobs {
TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull",
SecretScan = "secret-scan",
- UpgradeProjectToGhost = "upgrade-project-to-ghost-job"
+ UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
+ DynamicSecretRevocation = "dynamic-secret-revocation",
+ DynamicSecretPruning = "dynamic-secret-pruning"
}
export type TQueueJobTypes = {
@@ -58,11 +61,11 @@ export type TQueueJobTypes = {
};
[QueueName.SecretWebhook]: {
name: QueueJobs.SecWebhook;
- payload: { projectId: string; environment: string; secretPath: string };
+ payload: { projectId: string; environment: string; secretPath: string; depth?: number };
};
[QueueName.IntegrationSync]: {
name: QueueJobs.IntegrationSync;
- payload: { projectId: string; environment: string; secretPath: string };
+ payload: { projectId: string; environment: string; secretPath: string; depth?: number };
};
[QueueName.SecretFullRepoScan]: {
name: QueueJobs.SecretScan;
@@ -86,6 +89,19 @@ export type TQueueJobTypes = {
name: QueueJobs.TelemetryInstanceStats;
payload: undefined;
};
+ [QueueName.DynamicSecretRevocation]:
+ | {
+ name: QueueJobs.DynamicSecretRevocation;
+ payload: {
+ leaseId: string;
+ };
+ }
+ | {
+ name: QueueJobs.DynamicSecretPruning;
+ payload: {
+ dynamicSecretCfgId: string;
+ };
+ };
};
export type TQueueServiceFactory = ReturnType;
diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts
index 556a88d7c..51cef185a 100644
--- a/backend/src/server/app.ts
+++ b/backend/src/server/app.ts
@@ -24,6 +24,7 @@ import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
import { fastifyIp } from "./plugins/ip";
+import { maintenanceMode } from "./plugins/maintenanceMode";
import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes";
@@ -72,6 +73,8 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
}
await server.register(helmet, { contentSecurityPolicy: false });
+ await server.register(maintenanceMode);
+
await server.register(registerRoutes, { smtp, queue, db, keyStore });
if (appCfg.isProductionMode) {
diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts
index 444158cbf..d8069b9db 100644
--- a/backend/src/server/config/rateLimiter.ts
+++ b/backend/src/server/config/rateLimiter.ts
@@ -18,14 +18,43 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
};
};
-export const authRateLimit: RateLimitOptions = {
+// GET endpoints
+export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: 600,
keyGenerator: (req) => req.realIp
};
-export const passwordRateLimit: RateLimitOptions = {
+// POST, PATCH, PUT, DELETE endpoints
+export const writeLimit: RateLimitOptions = {
+ timeWindow: 60 * 1000,
+ max: 50,
+ keyGenerator: (req) => req.realIp
+};
+
+// special endpoints
+export const secretsLimit: RateLimitOptions = {
+ // secrets, folders, secret imports
timeWindow: 60 * 1000,
max: 600,
keyGenerator: (req) => req.realIp
};
+
+export const authRateLimit: RateLimitOptions = {
+ timeWindow: 60 * 1000,
+ max: 60,
+ keyGenerator: (req) => req.realIp
+};
+
+export const inviteUserRateLimit: RateLimitOptions = {
+ timeWindow: 60 * 1000,
+ max: 30,
+ keyGenerator: (req) => req.realIp
+};
+
+export const creationLimit: RateLimitOptions = {
+ // identity, project, org
+ timeWindow: 60 * 1000,
+ max: 30,
+ keyGenerator: (req) => req.realIp
+};
diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts
index 3a0a0ab39..4c0683797 100644
--- a/backend/src/server/plugins/auth/inject-identity.ts
+++ b/backend/src/server/plugins/auth/inject-identity.ts
@@ -6,42 +6,49 @@ import { TServiceTokens, TUsers } from "@app/db/schemas";
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
-import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
+import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
export type TAuthMode =
| {
- orgId?: string;
authMode: AuthMode.JWT;
actor: ActorType.USER;
userId: string;
tokenVersionId: string; // the session id of token used
user: TUsers;
+ orgId: string;
+ authMethod: AuthMethod;
}
| {
authMode: AuthMode.API_KEY;
+ authMethod: null;
actor: ActorType.USER;
userId: string;
user: TUsers;
- orgId?: string;
+ orgId: string;
}
| {
authMode: AuthMode.SERVICE_TOKEN;
serviceToken: TServiceTokens & { createdByEmail: string };
actor: ActorType.SERVICE;
serviceTokenId: string;
+ orgId: string;
+ authMethod: null;
}
| {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN;
actor: ActorType.IDENTITY;
identityId: string;
identityName: string;
+ orgId: string;
+ authMethod: null;
}
| {
authMode: AuthMode.SCIM_TOKEN;
actor: ActorType.SCIM_CLIENT;
scimTokenId: string;
orgId: string;
+ authMethod: null;
};
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
@@ -50,6 +57,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
}
const authHeader = req.headers?.authorization;
+
if (!authHeader) return { authMode: null, token: null };
const authTokenValue = authHeader.slice(7); // slice of after Bearer
@@ -71,6 +79,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
actor: ActorType.USER
} as const;
case AuthTokenType.API_KEY:
+ // throw new Error("API Key auth is no longer supported.");
return { authMode: AuthMode.API_KEY, token: decodedToken, actor: ActorType.USER } as const;
case AuthTokenType.IDENTITY_ACCESS_TOKEN:
return {
@@ -89,17 +98,30 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
}
};
+// ! Important: You can only 100% count on the `req.permission.orgId` field being present when the auth method is Identity Access Token (Machine Identity).
export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
+
+ if (req.url.includes("/api/v3/auth/")) {
+ return;
+ }
if (!authMode) return;
switch (authMode) {
case AuthMode.JWT: {
const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token);
- req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, orgId };
+ req.auth = {
+ authMode: AuthMode.JWT,
+ user,
+ userId: user.id,
+ tokenVersionId,
+ actor,
+ orgId: orgId as string,
+ authMethod: token.authMethod
+ };
break;
}
case AuthMode.IDENTITY_ACCESS_TOKEN: {
@@ -107,29 +129,40 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor,
+ orgId: identity.orgId,
identityId: identity.identityId,
- identityName: identity.name
+ identityName: identity.name,
+ authMethod: null
};
break;
}
case AuthMode.SERVICE_TOKEN: {
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
req.auth = {
+ orgId: serviceToken.orgId,
authMode: AuthMode.SERVICE_TOKEN as const,
serviceToken,
serviceTokenId: serviceToken.id,
- actor
+ actor,
+ authMethod: null
};
break;
}
case AuthMode.API_KEY: {
const user = await server.services.apiKey.fnValidateApiKey(token as string);
- req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
+ req.auth = {
+ authMode: AuthMode.API_KEY as const,
+ userId: user.id,
+ actor,
+ user,
+ orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon!
+ authMethod: null
+ };
break;
}
case AuthMode.SCIM_TOKEN: {
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
- req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId };
+ req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null };
break;
}
default:
diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts
index 2d61647e8..084f18198 100644
--- a/backend/src/server/plugins/auth/inject-permission.ts
+++ b/backend/src/server/plugins/auth/inject-permission.ts
@@ -9,13 +9,33 @@ export const injectPermission = fp(async (server) => {
if (!req.auth) return;
if (req.auth.actor === ActorType.USER) {
- req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId };
+ req.permission = {
+ type: ActorType.USER,
+ id: req.auth.userId,
+ orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY"
+ authMethod: req.auth.authMethod // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null
+ };
} else if (req.auth.actor === ActorType.IDENTITY) {
- req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
+ req.permission = {
+ type: ActorType.IDENTITY,
+ id: req.auth.identityId,
+ orgId: req.auth.orgId,
+ authMethod: null
+ };
} else if (req.auth.actor === ActorType.SERVICE) {
- req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
+ req.permission = {
+ type: ActorType.SERVICE,
+ id: req.auth.serviceTokenId,
+ orgId: req.auth.orgId,
+ authMethod: null
+ };
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
- req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId };
+ req.permission = {
+ type: ActorType.SCIM_CLIENT,
+ id: req.auth.scimTokenId,
+ orgId: req.auth.orgId,
+ authMethod: null
+ };
}
});
});
diff --git a/backend/src/server/plugins/auth/verify-auth.ts b/backend/src/server/plugins/auth/verify-auth.ts
index a1274f356..3b3a239f7 100644
--- a/backend/src/server/plugins/auth/verify-auth.ts
+++ b/backend/src/server/plugins/auth/verify-auth.ts
@@ -3,15 +3,26 @@ import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from "fastify";
import { UnauthorizedError } from "@app/lib/errors";
import { AuthMode } from "@app/services/auth/auth-type";
+interface TAuthOptions {
+ requireOrg: boolean;
+}
+
export const verifyAuth =
- (authStrats: AuthMode[]) =>
+ (authStrategies: AuthMode[], options: TAuthOptions = { requireOrg: true }) =>
(req: T, _res: FastifyReply, done: HookHandlerDoneFunction) => {
- if (!Array.isArray(authStrats)) throw new Error("Auth strategy must be array");
+ if (!Array.isArray(authStrategies)) throw new Error("Auth strategy must be array");
if (!req.auth) throw new UnauthorizedError({ name: "Unauthorized access", message: "Token missing" });
- const isAccessAllowed = authStrats.some((strat) => strat === req.auth.authMode);
+ const isAccessAllowed = authStrategies.some((strategy) => strategy === req.auth.authMode);
if (!isAccessAllowed) {
throw new UnauthorizedError({ name: `${req.url} Unauthorized Access` });
}
+
+ // New optional option. There are some routes which do not require an organization ID to be present on the request.
+ // An example of this is the /v1 auth routes.
+ if (req.auth.authMode === AuthMode.JWT && options.requireOrg === true && !req.permission.orgId) {
+ throw new UnauthorizedError({ name: `${req.url} Unauthorized Access, no organization found in request` });
+ }
+
done();
};
diff --git a/backend/src/server/plugins/maintenanceMode.ts b/backend/src/server/plugins/maintenanceMode.ts
new file mode 100644
index 000000000..f40f1ff6d
--- /dev/null
+++ b/backend/src/server/plugins/maintenanceMode.ts
@@ -0,0 +1,12 @@
+import fp from "fastify-plugin";
+
+import { getConfig } from "@app/lib/config/env";
+
+export const maintenanceMode = fp(async (fastify) => {
+ fastify.addHook("onRequest", async (req) => {
+ const serverEnvs = getConfig();
+ if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET" && serverEnvs.MAINTENANCE_MODE) {
+ throw new Error("Infisical is in maintenance mode. Please try again later.");
+ }
+ });
+});
diff --git a/backend/src/server/plugins/secret-scanner.ts b/backend/src/server/plugins/secret-scanner.ts
index 8790d54d4..d20008de7 100644
--- a/backend/src/server/plugins/secret-scanner.ts
+++ b/backend/src/server/plugins/secret-scanner.ts
@@ -4,6 +4,7 @@ import SmeeClient from "smee-client";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
+import { writeLimit } from "@app/server/config/rateLimiter";
export const registerSecretScannerGhApp = async (server: FastifyZodProvider) => {
const probotApp = (app: Probot) => {
@@ -49,6 +50,9 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
server.route({
method: "POST",
url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
handler: async (req, res) => {
const eventName = req.headers["x-github-event"];
const signatureSHA256 = req.headers["x-hub-signature-256"] as string;
diff --git a/backend/src/server/plugins/swagger.ts b/backend/src/server/plugins/swagger.ts
index 1eb1a0f4e..99032bb6f 100644
--- a/backend/src/server/plugins/swagger.ts
+++ b/backend/src/server/plugins/swagger.ts
@@ -14,13 +14,13 @@ export const fastifySwagger = fp(async (fastify) => {
version: "0.0.1"
},
servers: [
- {
- url: "http://localhost:8080",
- description: "Local server"
- },
{
url: "https://app.infisical.com",
description: "Production server"
+ },
+ {
+ url: "http://localhost:8080",
+ description: "Local server"
}
],
components: {
@@ -30,12 +30,6 @@ export const fastifySwagger = fp(async (fastify) => {
scheme: "bearer",
bearerFormat: "JWT",
description: "An access token in Infisical"
- },
- apiKeyAuth: {
- type: "apiKey",
- in: "header",
- name: "X-API-Key",
- description: "An API Key in Infisical"
}
}
}
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 0a49806bd..5d77c340b 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -5,12 +5,25 @@ import { registerV1EERoutes } from "@app/ee/routes/v1";
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-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";
+import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
+import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
+import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
+import { groupDALFactory } from "@app/ee/services/group/group-dal";
+import { groupServiceFactory } from "@app/ee/services/group/group-service";
+import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
+import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
+import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { 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 { 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";
@@ -39,6 +52,7 @@ import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
+import { readLimit } from "@app/server/config/rateLimiter";
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { authDALFactory } from "@app/services/auth/auth-dal";
@@ -47,6 +61,9 @@ 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 { 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";
import { identityDALFactory } from "@app/services/identity/identity-dal";
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { identityServiceFactory } from "@app/services/identity/identity-service";
@@ -143,6 +160,7 @@ export const registerRoutes = async (
const projectDAL = projectDALFactory(db);
const projectMembershipDAL = projectMembershipDALFactory(db);
+ const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
const projectRoleDAL = projectRoleDALFactory(db);
const projectEnvDAL = projectEnvDALFactory(db);
@@ -168,6 +186,7 @@ export const registerRoutes = async (
const identityOrgMembershipDAL = identityOrgDALFactory(db);
const identityProjectDAL = identityProjectDALFactory(db);
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
+ const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityUaDAL = identityUaDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
@@ -194,14 +213,21 @@ export const registerRoutes = async (
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
const gitAppOrgDAL = gitAppDALFactory(db);
+ const groupDAL = groupDALFactory(db);
+ const groupProjectDAL = groupProjectDALFactory(db);
+ const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
+ const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
const secretScanningDAL = secretScanningDALFactory(db);
const licenseDAL = licenseDALFactory(db);
+ const dynamicSecretDAL = dynamicSecretDALFactory(db);
+ const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
projectRoleDAL,
- serviceTokenDAL
+ serviceTokenDAL,
+ projectDAL
});
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
const trustedIpService = trustedIpServiceFactory({
@@ -233,6 +259,29 @@ export const registerRoutes = async (
samlConfigDAL,
licenseService
});
+ const groupService = groupServiceFactory({
+ userDAL,
+ groupDAL,
+ groupProjectDAL,
+ orgDAL,
+ userGroupMembershipDAL,
+ projectDAL,
+ projectBotDAL,
+ projectKeyDAL,
+ permissionService,
+ licenseService
+ });
+ const groupProjectService = groupProjectServiceFactory({
+ groupDAL,
+ groupProjectDAL,
+ groupProjectMembershipRoleDAL,
+ userGroupMembershipDAL,
+ projectDAL,
+ projectKeyDAL,
+ projectBotDAL,
+ projectRoleDAL,
+ permissionService
+ });
const scimService = scimServiceFactory({
licenseService,
scimDAL,
@@ -240,6 +289,7 @@ export const registerRoutes = async (
orgDAL,
projectDAL,
projectMembershipDAL,
+ groupDAL,
permissionService,
smtpService
});
@@ -266,7 +316,7 @@ export const registerRoutes = async (
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
const userService = userServiceFactory({ userDAL });
- const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService });
+ const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
const passwordService = authPaswordServiceFactory({
tokenService,
smtpService,
@@ -286,6 +336,7 @@ export const registerRoutes = async (
projectKeyDAL,
smtpService,
userDAL,
+ groupDAL,
orgBotDAL
});
const signupService = authSignupServiceFactory({
@@ -331,11 +382,17 @@ export const registerRoutes = async (
projectBotDAL,
orgDAL,
userDAL,
+ userGroupMembershipDAL,
smtpService,
projectKeyDAL,
projectRoleDAL,
licenseService
});
+ const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
+ permissionService,
+ projectMembershipDAL,
+ projectUserAdditionalPrivilegeDAL
+ });
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDAL,
@@ -372,12 +429,14 @@ export const registerRoutes = async (
projectKeyDAL,
userDAL,
projectEnvDAL,
+ orgDAL,
orgService,
projectMembershipDAL,
folderDAL,
licenseService,
projectUserMembershipRoleDAL,
- identityProjectMembershipRoleDAL
+ identityProjectMembershipRoleDAL,
+ keyStore
});
const projectEnvService = projectEnvServiceFactory({
@@ -388,7 +447,12 @@ export const registerRoutes = async (
folderDAL
});
- const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
+ const projectRoleService = projectRoleServiceFactory({
+ permissionService,
+ projectRoleDAL,
+ projectUserMembershipRoleDAL,
+ identityProjectMembershipRoleDAL
+ });
const snapshotService = secretSnapshotServiceFactory({
permissionService,
@@ -417,14 +481,6 @@ export const registerRoutes = async (
projectEnvDAL,
snapshotService
});
- const secretImportService = secretImportServiceFactory({
- projectEnvDAL,
- folderDAL,
- permissionService,
- secretImportDAL,
- projectDAL,
- secretDAL
- });
const integrationAuthService = integrationAuthServiceFactory({
integrationAuthDAL,
integrationDAL,
@@ -452,6 +508,15 @@ export const registerRoutes = async (
secretTagDAL,
secretVersionTagDAL
});
+ const secretImportService = secretImportServiceFactory({
+ projectEnvDAL,
+ folderDAL,
+ permissionService,
+ secretImportDAL,
+ projectDAL,
+ secretDAL,
+ secretQueueService
+ });
const secretBlindIndexService = secretBlindIndexServiceFactory({
permissionService,
secretDAL,
@@ -469,6 +534,7 @@ export const registerRoutes = async (
snapshotService,
secretQueueService,
secretImportDAL,
+ projectEnvDAL,
projectBotService
});
const sarService = secretApprovalRequestServiceFactory({
@@ -517,7 +583,8 @@ export const registerRoutes = async (
projectEnvDAL,
serviceTokenDAL,
userDAL,
- permissionService
+ permissionService,
+ projectDAL
});
const identityService = identityServiceFactory({
@@ -525,7 +592,10 @@ export const registerRoutes = async (
identityDAL,
identityOrgMembershipDAL
});
- const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL });
+ const identityAccessTokenService = identityAccessTokenServiceFactory({
+ identityAccessTokenDAL,
+ identityOrgMembershipDAL
+ });
const identityProjectService = identityProjectServiceFactory({
permissionService,
projectDAL,
@@ -534,6 +604,12 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL,
projectRoleDAL
});
+ const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({
+ projectDAL,
+ identityProjectAdditionalPrivilegeDAL,
+ permissionService,
+ identityProjectDAL
+ });
const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL,
permissionService,
@@ -544,6 +620,34 @@ export const registerRoutes = async (
licenseService
});
+ const dynamicSecretProviders = buildDynamicSecretProviders();
+ const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
+ queueService,
+ dynamicSecretLeaseDAL,
+ dynamicSecretProviders,
+ dynamicSecretDAL
+ });
+ const dynamicSecretService = dynamicSecretServiceFactory({
+ projectDAL,
+ dynamicSecretQueueService,
+ dynamicSecretDAL,
+ dynamicSecretLeaseDAL,
+ dynamicSecretProviders,
+ folderDAL,
+ permissionService,
+ licenseService
+ });
+ const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
+ projectDAL,
+ permissionService,
+ dynamicSecretQueueService,
+ dynamicSecretDAL,
+ dynamicSecretLeaseDAL,
+ dynamicSecretProviders,
+ folderDAL,
+ licenseService
+ });
+
await superAdminService.initServerCfg();
//
// setup the communication with license key server
@@ -558,6 +662,8 @@ export const registerRoutes = async (
password: passwordService,
signup: signupService,
user: userService,
+ group: groupService,
+ groupProject: groupProjectService,
permission: permissionService,
org: orgService,
orgRole: orgRoleService,
@@ -585,6 +691,8 @@ export const registerRoutes = async (
secretApprovalPolicy: sapService,
secretApprovalRequest: sarService,
secretRotation: secretRotationService,
+ dynamicSecret: dynamicSecretService,
+ dynamicSecretLease: dynamicSecretLeaseService,
snapshot: snapshotService,
saml: samlService,
ldap: ldapService,
@@ -594,7 +702,9 @@ export const registerRoutes = async (
trustedIp: trustedIpService,
scim: scimService,
secretBlindIndex: secretBlindIndexService,
- telemetry: telemetryService
+ telemetry: telemetryService,
+ projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
+ identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService
});
server.decorate("store", {
@@ -606,8 +716,11 @@ export const registerRoutes = async (
await server.register(injectAuditLogInfo);
server.route({
- url: "/api/status",
method: "GET",
+ url: "/api/status",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: z.object({
diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts
index 03e48c247..eaae4149c 100644
--- a/backend/src/server/routes/sanitizedSchemas.ts
+++ b/backend/src/server/routes/sanitizedSchemas.ts
@@ -1,6 +1,11 @@
import { z } from "zod";
-import { IntegrationAuthsSchema, SecretApprovalPoliciesSchema, UsersSchema } from "@app/db/schemas";
+import {
+ DynamicSecretsSchema,
+ IntegrationAuthsSchema,
+ SecretApprovalPoliciesSchema,
+ UsersSchema
+} from "@app/db/schemas";
// sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod
@@ -56,3 +61,11 @@ export const secretRawSchema = z.object({
secretValue: z.string(),
secretComment: z.string().optional()
});
+
+export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
+ inputIV: true,
+ inputTag: true,
+ inputCiphertext: true,
+ keyEncoding: true,
+ algorithm: true
+});
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index 1a048e945..e70822128 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
+import { readLimit, writeLimit } 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";
@@ -11,24 +12,33 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerAdminRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/config",
method: "GET",
+ url: "/config",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: z.object({
- config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true })
+ config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).merge(
+ z.object({ isMigrationModeOn: z.boolean() })
+ )
})
}
},
handler: async () => {
const config = await getServerCfg();
- return { config };
+ const serverEnvs = getConfig();
+ return { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE } };
}
});
server.route({
- url: "/config",
method: "PATCH",
+ url: "/config",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
allowSignUp: z.boolean().optional(),
@@ -52,8 +62,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/signup",
method: "POST",
+ url: "/signup",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
email: z.string().email().trim(),
diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts
index bd45f59d9..7f09a904b 100644
--- a/backend/src/server/routes/v1/auth-router.ts
+++ b/backend/src/server/routes/v1/auth-router.ts
@@ -3,7 +3,7 @@ import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
-import { authRateLimit } from "@app/server/config/rateLimiter";
+import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
@@ -21,7 +21,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req, res) => {
const appCfg = getConfig();
if (req.auth.authMode === AuthMode.JWT) {
@@ -38,8 +38,11 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/checkAuth",
method: "POST",
+ url: "/checkAuth",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
response: {
200: z.object({
@@ -52,8 +55,11 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/token",
method: "POST",
+ url: "/token",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
response: {
200: z.object({
@@ -85,6 +91,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
const token = jwt.sign(
{
+ authMethod: decodedToken.authMethod,
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: decodedToken.userId,
tokenVersionId: tokenVersion.id,
diff --git a/backend/src/server/routes/v1/bot-router.ts b/backend/src/server/routes/v1/bot-router.ts
index 507423b0d..34a34f843 100644
--- a/backend/src/server/routes/v1/bot-router.ts
+++ b/backend/src/server/routes/v1/bot-router.ts
@@ -1,13 +1,17 @@
import { z } from "zod";
import { ProjectBotsSchema } from "@app/db/schemas";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:projectId",
method: "GET",
+ url: "/:projectId",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -30,6 +34,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
projectId: req.params.projectId
});
return { bot };
@@ -37,8 +42,11 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:botId/active",
method: "PATCH",
+ url: "/:botId/active",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
isActive: z.boolean(),
@@ -70,6 +78,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
botId: req.params.botId,
botKey: req.body.botKey,
isActive: req.body.isActive
diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts
index 78112f896..387c54c13 100644
--- a/backend/src/server/routes/v1/identity-access-token-router.ts
+++ b/backend/src/server/routes/v1/identity-access-token-router.ts
@@ -1,13 +1,19 @@
import { z } from "zod";
+import { UNIVERSAL_AUTH } from "@app/lib/api-docs";
+import { writeLimit } from "@app/server/config/rateLimiter";
+
export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/token/renew",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Renew access token",
body: z.object({
- accessToken: z.string().trim()
+ accessToken: z.string().trim().describe(UNIVERSAL_AUTH.RENEW_ACCESS_TOKEN.accessToken)
}),
response: {
200: z.object({
diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts
index 0ec27a98b..e174cf974 100644
--- a/backend/src/server/routes/v1/identity-router.ts
+++ b/backend/src/server/routes/v1/identity-router.ts
@@ -2,6 +2,8 @@ import { z } from "zod";
import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { IDENTITIES } from "@app/lib/api-docs";
+import { creationLimit, 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";
@@ -11,6 +13,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
+ config: {
+ rateLimit: creationLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create identity",
@@ -20,9 +25,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
],
body: z.object({
- name: z.string().trim(),
- organizationId: z.string().trim(),
- role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess)
+ name: z.string().trim().describe(IDENTITIES.CREATE.name),
+ organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
+ role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role)
}),
response: {
200: z.object({
@@ -34,6 +39,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
const identity = await server.services.identity.createIdentity({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
orgId: req.body.organizationId
@@ -69,6 +75,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "PATCH",
url: "/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update identity",
@@ -78,11 +87,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string()
+ identityId: z.string().describe(IDENTITIES.UPDATE.identityId)
}),
body: z.object({
- name: z.string().trim().optional(),
- role: z.string().trim().min(1).optional()
+ name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
+ role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role)
}),
response: {
200: z.object({
@@ -94,6 +103,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
const identity = await server.services.identity.updateIdentity({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.identityId,
...req.body
@@ -118,6 +128,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete identity",
@@ -127,7 +140,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string()
+ identityId: z.string().describe(IDENTITIES.DELETE.identityId)
}),
response: {
200: z.object({
@@ -139,6 +152,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
const identity = await server.services.identity.deleteIdentity({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.identityId
});
diff --git a/backend/src/server/routes/v1/identity-ua.ts b/backend/src/server/routes/v1/identity-ua.ts
index 4499a88e7..670f52416 100644
--- a/backend/src/server/routes/v1/identity-ua.ts
+++ b/backend/src/server/routes/v1/identity-ua.ts
@@ -2,6 +2,8 @@ import { z } from "zod";
import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { UNIVERSAL_AUTH } 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 { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -21,13 +23,16 @@ export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({
export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/universal-auth/login",
method: "POST",
+ url: "/universal-auth/login",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Login with Universal Auth",
body: z.object({
- clientId: z.string().trim(),
- clientSecret: z.string().trim()
+ clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId),
+ clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret)
}),
response: {
200: z.object({
@@ -65,8 +70,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/universal-auth/identities/:identityId",
method: "POST",
+ url: "/universal-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach Universal Auth configuration onto identity",
@@ -76,7 +84,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string().trim()
+ identityId: z.string().trim().describe(UNIVERSAL_AUTH.ATTACH.identityId)
}),
body: z.object({
clientSecretTrustedIps: z
@@ -85,14 +93,16 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
})
.array()
.min(1)
- .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
+ .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
+ .describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
- .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
+ .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
+ .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
@@ -100,15 +110,22 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
- .default(2592000),
+ .default(2592000)
+ .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
- .default(2592000), // 30 days
- accessTokenNumUsesLimit: z.number().int().min(0).default(0)
+ .default(2592000)
+ .describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days
+ accessTokenNumUsesLimit: z
+ .number()
+ .int()
+ .min(0)
+ .default(0)
+ .describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
response: {
200: z.object({
@@ -121,6 +138,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
});
@@ -145,8 +163,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/universal-auth/identities/:identityId",
method: "PATCH",
+ url: "/universal-auth/identities/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update Universal Auth configuration on identity",
@@ -156,7 +177,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string()
+ identityId: z.string().describe(UNIVERSAL_AUTH.UPDATE.identityId)
}),
body: z.object({
clientSecretTrustedIps: z
@@ -165,16 +186,23 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
})
.array()
.min(1)
- .optional(),
+ .optional()
+ .describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps),
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(),
+ .optional()
+ .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps),
+ accessTokenTTL: z.number().int().min(0).optional().describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL),
+ accessTokenNumUsesLimit: z
+ .number()
+ .int()
+ .min(0)
+ .optional()
+ .describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
@@ -182,6 +210,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
+ .describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL)
}),
response: {
200: z.object({
@@ -194,6 +223,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
});
@@ -219,8 +249,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/universal-auth/identities/:identityId",
method: "GET",
+ url: "/universal-auth/identities/:identityId",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve Universal Auth configuration on identity",
@@ -230,7 +263,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string()
+ identityId: z.string().describe(UNIVERSAL_AUTH.RETRIEVE.identityId)
}),
response: {
200: z.object({
@@ -242,6 +275,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
const identityUniversalAuth = await server.services.identityUa.getIdentityUa({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
@@ -262,8 +296,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/universal-auth/identities/:identityId/client-secrets",
method: "POST",
+ url: "/universal-auth/identities/:identityId/client-secrets",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create Universal Auth Client Secret for identity",
@@ -273,12 +310,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string()
+ identityId: z.string().describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.identityId)
}),
body: z.object({
- description: z.string().trim().default(""),
- numUsesLimit: z.number().min(0).default(0),
- ttl: z.number().min(0).default(0)
+ description: z.string().trim().default("").describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.description),
+ numUsesLimit: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.numUsesLimit),
+ ttl: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.ttl)
}),
response: {
200: z.object({
@@ -291,6 +328,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
...req.body
@@ -313,8 +351,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/universal-auth/identities/:identityId/client-secrets",
method: "GET",
+ url: "/universal-auth/identities/:identityId/client-secrets",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "List Universal Auth Client Secrets for identity",
@@ -324,7 +365,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string()
+ identityId: z.string().describe(UNIVERSAL_AUTH.LIST_CLIENT_SECRETS.identityId)
}),
response: {
200: z.object({
@@ -336,6 +377,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
@@ -355,8 +397,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke",
method: "POST",
+ url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Revoke Universal Auth Client Secrets for identity",
@@ -366,8 +411,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
- identityId: z.string(),
- clientSecretId: z.string()
+ identityId: z.string().describe(UNIVERSAL_AUTH.REVOKE_CLIENT_SECRET.identityId),
+ clientSecretId: z.string().describe(UNIVERSAL_AUTH.REVOKE_CLIENT_SECRET.clientSecretId)
}),
response: {
200: z.object({
@@ -379,6 +424,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
const clientSecretData = await server.services.identityUa.revokeUaClientSecret({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
clientSecretId: req.params.clientSecretId
diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts
index 3f48a39d4..d9db7404e 100644
--- a/backend/src/server/routes/v1/integration-auth-router.ts
+++ b/backend/src/server/routes/v1/integration-auth-router.ts
@@ -1,6 +1,8 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { INTEGRATION_AUTH } 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";
@@ -8,10 +10,19 @@ import { integrationAuthPubSchema } from "../sanitizedSchemas";
export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/integration-options",
method: "GET",
- onRequest: verifyAuth([AuthMode.JWT]),
+ url: "/integration-options",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
+ description: "List of integrations available.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
response: {
200: z.object({
integrationOptions: z
@@ -36,12 +47,21 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId",
method: "GET",
- onRequest: verifyAuth([AuthMode.JWT]),
+ url: "/:integrationAuthId",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
+ description: "Get details of an integration authorization by auth object id.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- integrationAuthId: z.string().trim()
+ integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.GET.integrationAuthId)
}),
response: {
200: z.object({
@@ -53,6 +73,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const integrationAuth = await server.services.integrationAuth.getIntegrationAuth({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
@@ -61,13 +82,22 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/",
method: "DELETE",
- onRequest: verifyAuth([AuthMode.JWT]),
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
+ description: "Remove all integration's auth object from the project.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
querystring: z.object({
- integration: z.string().trim(),
- projectId: z.string().trim()
+ integration: z.string().trim().describe(INTEGRATION_AUTH.DELETE.integration),
+ projectId: z.string().trim().describe(INTEGRATION_AUTH.DELETE.projectId)
}),
response: {
200: z.object({
@@ -80,6 +110,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
integration: req.query.integration,
projectId: req.query.projectId
});
@@ -100,12 +131,21 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId",
method: "DELETE",
- onRequest: verifyAuth([AuthMode.JWT]),
+ url: "/:integrationAuthId",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
+ description: "Remove an integration auth object by object id.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- integrationAuthId: z.string().trim()
+ integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.DELETE_BY_ID.integrationAuthId)
}),
response: {
200: z.object({
@@ -117,6 +157,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const integrationAuth = await server.services.integrationAuth.deleteIntegrationAuthById({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
@@ -137,8 +178,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/oauth-token",
method: "POST",
+ url: "/oauth-token",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
@@ -157,6 +201,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const integrationAuth = await server.services.integrationAuth.oauthExchange({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...req.body
@@ -177,18 +222,27 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/access-token",
method: "POST",
- onRequest: verifyAuth([AuthMode.JWT]),
+ url: "/access-token",
+ config: {
+ rateLimit: writeLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
+ description: "Create the integration authentication object required for syncing secrets.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
body: z.object({
- workspaceId: z.string().trim(),
- integration: z.string().trim(),
- accessId: z.string().trim().optional(),
- accessToken: z.string().trim().optional(),
- url: z.string().url().trim().optional(),
- namespace: z.string().trim().optional(),
- refreshToken: z.string().trim().optional()
+ workspaceId: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.workspaceId),
+ integration: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration),
+ accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId),
+ accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken),
+ url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url),
+ namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace),
+ refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken)
}),
response: {
200: z.object({
@@ -200,6 +254,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const integrationAuth = await server.services.integrationAuth.saveIntegrationToken({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...req.body
@@ -220,8 +275,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/apps",
method: "GET",
+ url: "/:integrationAuthId/apps",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -247,6 +305,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const apps = await server.services.integrationAuth.getIntegrationApps({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
...req.query
@@ -256,8 +315,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/teams",
method: "GET",
+ url: "/:integrationAuthId/teams",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -278,6 +340,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const teams = await server.services.integrationAuth.getIntegrationAuthTeams({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
@@ -286,8 +349,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/vercel/branches",
method: "GET",
+ url: "/:integrationAuthId/vercel/branches",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -306,6 +372,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const branches = await server.services.integrationAuth.getVercelBranches({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
appId: req.query.appId
@@ -315,8 +382,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/checkly/groups",
method: "GET",
+ url: "/:integrationAuthId/checkly/groups",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -335,6 +405,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const groups = await server.services.integrationAuth.getChecklyGroups({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
accountId: req.query.accountId
@@ -344,8 +415,79 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/qovery/orgs",
method: "GET",
+ url: "/:integrationAuthId/github/orgs",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ integrationAuthId: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ orgs: z.object({ name: z.string(), orgId: z.string() }).array()
+ })
+ }
+ },
+ handler: async (req) => {
+ const orgs = await server.services.integrationAuth.getGithubOrgs({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ id: req.params.integrationAuthId
+ });
+ if (!orgs) throw new Error("No organization found.");
+
+ return { orgs };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:integrationAuthId/github/envs",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ integrationAuthId: z.string().trim()
+ }),
+ querystring: z.object({
+ repoOwner: z.string().trim(),
+ repoName: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ envs: z.object({ name: z.string(), envId: z.string() }).array()
+ })
+ }
+ },
+ handler: async (req) => {
+ const envs = await server.services.integrationAuth.getGithubEnvs({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ id: req.params.integrationAuthId,
+ actorAuthMethod: req.permission.authMethod,
+ repoName: req.query.repoName,
+ repoOwner: req.query.repoOwner
+ });
+ if (!envs) throw new Error("No organization found.");
+
+ return { envs };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:integrationAuthId/qovery/orgs",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -361,6 +503,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const orgs = await server.services.integrationAuth.getQoveryOrgs({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
@@ -369,8 +512,44 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/qovery/projects",
method: "GET",
+ url: "/:integrationAuthId/aws-secrets-manager/kms-keys",
+ config: {
+ rateLimit: readLimit
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ schema: {
+ params: z.object({
+ integrationAuthId: z.string().trim()
+ }),
+ querystring: z.object({
+ region: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ kmsKeys: z.object({ id: z.string(), alias: z.string() }).array()
+ })
+ }
+ },
+ handler: async (req) => {
+ const kmsKeys = await server.services.integrationAuth.getAwsKmsKeys({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ id: req.params.integrationAuthId,
+ region: req.query.region
+ });
+ return { kmsKeys };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:integrationAuthId/qovery/projects",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -389,6 +568,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const projects = await server.services.integrationAuth.getQoveryProjects({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
orgId: req.query.orgId
@@ -398,8 +578,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/qovery/environments",
method: "GET",
+ url: "/:integrationAuthId/qovery/environments",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -418,6 +601,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const environments = await server.services.integrationAuth.getQoveryEnvs({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
projectId: req.query.projectId
@@ -427,8 +611,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/qovery/apps",
method: "GET",
+ url: "/:integrationAuthId/qovery/apps",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -447,6 +634,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const apps = await server.services.integrationAuth.getQoveryApps({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
environmentId: req.query.environmentId
@@ -456,8 +644,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/qovery/containers",
method: "GET",
+ url: "/:integrationAuthId/qovery/containers",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -476,6 +667,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const containers = await server.services.integrationAuth.getQoveryContainers({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
environmentId: req.query.environmentId
@@ -485,8 +677,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/qovery/jobs",
method: "GET",
+ url: "/:integrationAuthId/qovery/jobs",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -505,6 +700,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const jobs = await server.services.integrationAuth.getQoveryJobs({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
environmentId: req.query.environmentId
@@ -514,8 +710,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/heroku/pipelines",
method: "GET",
+ url: "/:integrationAuthId/heroku/pipelines",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -537,6 +736,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const pipelines = await server.services.integrationAuth.getHerokuPipelines({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
@@ -545,8 +745,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/railway/environments",
method: "GET",
+ url: "/:integrationAuthId/railway/environments",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -565,6 +768,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const environments = await server.services.integrationAuth.getRailwayEnvironments({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
appId: req.query.appId
@@ -574,8 +778,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/railway/services",
method: "GET",
+ url: "/:integrationAuthId/railway/services",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -594,6 +801,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const services = await server.services.integrationAuth.getRailwayServices({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
appId: req.query.appId
@@ -603,8 +811,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/bitbucket/workspaces",
method: "GET",
+ url: "/:integrationAuthId/bitbucket/workspaces",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -630,6 +841,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const workspaces = await server.services.integrationAuth.getBitbucketWorkspaces({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
@@ -638,8 +850,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/northflank/secret-groups",
method: "GET",
+ url: "/:integrationAuthId/northflank/secret-groups",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -663,6 +878,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const secretGroups = await server.services.integrationAuth.getNorthFlankSecretGroups({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
appId: req.query.appId
@@ -672,8 +888,11 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:integrationAuthId/teamcity/build-configs",
method: "GET",
+ url: "/:integrationAuthId/teamcity/build-configs",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -697,6 +916,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
const buildConfigs = await server.services.integrationAuth.getTeamcityBuildConfigs({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
appId: req.query.appId
diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts
index ed1914ccc..f908aa1fc 100644
--- a/backend/src/server/routes/v1/integration-router.ts
+++ b/backend/src/server/routes/v1/integration-router.ts
@@ -2,7 +2,9 @@ import { z } from "zod";
import { IntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { INTEGRATION } from "@app/lib/api-docs";
import { removeTrailingSlash, shake } from "@app/lib/fn";
+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";
@@ -10,37 +12,63 @@ import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telem
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/",
method: "POST",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
+ description: "Create an integration to sync secrets.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
body: z.object({
- integrationAuthId: z.string().trim(),
- app: z.string().trim().optional(),
- isActive: z.boolean(),
- appId: z.string().trim().optional(),
- secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
- sourceEnvironment: z.string().trim(),
- targetEnvironment: z.string().trim().optional(),
- targetEnvironmentId: z.string().trim().optional(),
- targetService: z.string().trim().optional(),
- targetServiceId: z.string().trim().optional(),
- owner: z.string().trim().optional(),
- path: z.string().trim().optional(),
- region: z.string().trim().optional(),
- scope: z.string().trim().optional(),
+ integrationAuthId: z.string().trim().describe(INTEGRATION.CREATE.integrationAuthId),
+ app: z.string().trim().optional().describe(INTEGRATION.CREATE.app),
+ isActive: z.boolean().describe(INTEGRATION.CREATE.isActive).default(true),
+ appId: z.string().trim().optional().describe(INTEGRATION.CREATE.appId),
+ secretPath: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(INTEGRATION.CREATE.secretPath),
+ sourceEnvironment: z.string().trim().describe(INTEGRATION.CREATE.sourceEnvironment),
+ targetEnvironment: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironment),
+ targetEnvironmentId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironmentId),
+ 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),
+ 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(),
- secretSuffix: z.string().optional(),
- initialSyncBehavior: z.string().optional(),
+ 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),
+ 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)
})
- .optional()
+ .default({})
}),
response: {
200: z.object({
@@ -48,11 +76,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { integration, integrationAuth } = await server.services.integration.createIntegration({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
@@ -97,20 +126,34 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:integrationId",
method: "PATCH",
+ url: "/:integrationId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
+ description: "Update an integration by integration id",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- integrationId: z.string().trim()
+ integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
}),
body: z.object({
- app: z.string().trim(),
- appId: z.string().trim(),
- isActive: z.boolean(),
- secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
- targetEnvironment: z.string().trim(),
- owner: z.string().trim(),
- environment: z.string().trim()
+ app: z.string().trim().describe(INTEGRATION.UPDATE.app),
+ appId: z.string().trim().describe(INTEGRATION.UPDATE.appId),
+ isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
+ secretPath: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(INTEGRATION.UPDATE.secretPath),
+ targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
+ owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
+ environment: z.string().trim().describe(INTEGRATION.UPDATE.environment)
}),
response: {
200: z.object({
@@ -118,11 +161,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integration = await server.services.integration.updateIntegration({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationId,
...req.body
@@ -132,11 +176,20 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:integrationId",
method: "DELETE",
+ url: "/:integrationId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
+ description: "Remove an integration using the integration object ID",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- integrationId: z.string().trim()
+ integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
}),
response: {
200: z.object({
@@ -144,10 +197,11 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integration = await server.services.integration.deleteIntegration({
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts
index 5956b53df..873710f10 100644
--- a/backend/src/server/routes/v1/invite-org-router.ts
+++ b/backend/src/server/routes/v1/invite-org-router.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { UsersSchema } from "@app/db/schemas";
+import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -9,6 +10,9 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/signup",
+ config: {
+ rateLimit: inviteUserRateLimit
+ },
method: "POST",
schema: {
body: z.object({
@@ -29,6 +33,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
orgId: req.body.organizationId,
userId: req.permission.id,
inviteeEmail: req.body.inviteeEmail,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
@@ -51,6 +56,9 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/verify",
method: "POST",
+ config: {
+ rateLimit: inviteUserRateLimit
+ },
schema: {
body: z.object({
email: z.string().trim().email(),
diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts
index d31682d88..808f125bb 100644
--- a/backend/src/server/routes/v1/organization-router.ts
+++ b/backend/src/server/routes/v1/organization-router.ts
@@ -1,6 +1,15 @@
import { z } from "zod";
-import { IncidentContactsSchema, OrganizationsSchema, OrgMembershipsSchema, UsersSchema } from "@app/db/schemas";
+import {
+ GroupsSchema,
+ IncidentContactsSchema,
+ OrganizationsSchema,
+ OrgMembershipsSchema,
+ OrgRolesSchema,
+ UsersSchema
+} from "@app/db/schemas";
+import { ORGANIZATIONS } 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";
@@ -8,6 +17,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: z.object({
@@ -15,7 +27,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const organizations = await server.services.org.findAllOrganizationOfUser(req.permission.id);
return { organizations };
@@ -25,6 +37,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -40,6 +55,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const organization = await server.services.org.findOrganizationById(
req.permission.id,
req.params.organizationId,
+ req.permission.authMethod,
req.permission.orgId
);
return { organization };
@@ -49,6 +65,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/users",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -76,6 +95,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const users = await server.services.org.findAllOrgMembers(
req.permission.id,
req.params.organizationId,
+ req.permission.authMethod,
req.permission.orgId
);
return { users };
@@ -85,6 +105,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "PATCH",
url: "/:organizationId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
@@ -111,6 +134,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
data: req.body
});
@@ -125,6 +149,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/incidentContactOrg",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
response: {
@@ -138,6 +165,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const incidentContactsOrg = await req.server.services.org.findIncidentContacts(
req.permission.id,
req.params.organizationId,
+ req.permission.authMethod,
req.permission.orgId
);
return { incidentContactsOrg };
@@ -147,6 +175,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:organizationId/incidentContactOrg",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({ email: z.string().email().trim() }),
@@ -162,6 +193,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
req.permission.id,
req.params.organizationId,
req.body.email,
+ req.permission.authMethod,
req.permission.orgId
);
return { incidentContactsOrg };
@@ -171,6 +203,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:organizationId/incidentContactOrg/:incidentContactId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }),
response: {
@@ -185,9 +220,47 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
req.permission.id,
req.params.organizationId,
req.params.incidentContactId,
+ req.permission.authMethod,
req.permission.orgId
);
return { incidentContactsOrg };
}
});
+
+ server.route({
+ method: "GET",
+ url: "/:organizationId/groups",
+ schema: {
+ params: z.object({
+ organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_GROUPS.organizationId)
+ }),
+ response: {
+ 200: z.object({
+ groups: GroupsSchema.merge(
+ z.object({
+ customRole: OrgRolesSchema.pick({
+ id: true,
+ name: true,
+ slug: true,
+ permissions: true,
+ description: true
+ }).optional()
+ })
+ ).array()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const groups = await server.services.org.getOrgGroups({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ orgId: req.params.organizationId,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ return { groups };
+ }
+ });
};
diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts
index d5c5054df..a8ef3fb77 100644
--- a/backend/src/server/routes/v1/password-router.ts
+++ b/backend/src/server/routes/v1/password-router.ts
@@ -2,7 +2,7 @@ import { z } from "zod";
import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
-import { passwordRateLimit } from "@app/server/config/rateLimiter";
+import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -12,7 +12,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/srp1",
config: {
- rateLimit: passwordRateLimit
+ rateLimit: authRateLimit
},
schema: {
body: z.object({
@@ -39,7 +39,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/change-password",
config: {
- rateLimit: passwordRateLimit
+ rateLimit: authRateLimit
},
schema: {
body: z.object({
@@ -78,7 +78,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/email/password-reset",
config: {
- rateLimit: passwordRateLimit
+ rateLimit: authRateLimit
},
schema: {
body: z.object({
@@ -103,7 +103,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/email/password-reset-verify",
config: {
- rateLimit: passwordRateLimit
+ rateLimit: authRateLimit
},
schema: {
body: z.object({
@@ -133,7 +133,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/backup-private-key",
config: {
- rateLimit: passwordRateLimit
+ rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
@@ -168,7 +168,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "GET",
url: "/backup-private-key",
config: {
- rateLimit: passwordRateLimit
+ rateLimit: authRateLimit
},
schema: {
response: {
@@ -190,6 +190,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/password-reset",
+ config: {
+ rateLimit: authRateLimit
+ },
schema: {
body: z.object({
protectedKey: z.string().trim(),
diff --git a/backend/src/server/routes/v1/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts
index db35e16f6..341b8a184 100644
--- a/backend/src/server/routes/v1/project-env-router.ts
+++ b/backend/src/server/routes/v1/project-env-router.ts
@@ -3,32 +3,37 @@ import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { ENVIRONMENTS } from "@app/lib/api-docs";
+import { 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 registerProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:workspaceId/environments",
method: "POST",
+ url: "/:workspaceId/environments",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Create environment",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(ENVIRONMENTS.CREATE.workspaceId)
}),
body: z.object({
- name: z.string().trim(),
+ name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
+ .describe(ENVIRONMENTS.CREATE.slug)
}),
response: {
200: z.object({
@@ -44,6 +49,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
...req.body
});
@@ -68,28 +74,33 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/environments/:id",
method: "PATCH",
+ url: "/:workspaceId/environments/:id",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Update environment",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- workspaceId: z.string().trim(),
- id: z.string().trim()
+ workspaceId: z.string().trim().describe(ENVIRONMENTS.UPDATE.workspaceId),
+ id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
}),
body: z.object({
slug: z
.string()
- .regex(/^[^./]*$/g)
.trim()
- .optional(),
- name: z.string().trim().optional(),
- position: z.number().optional()
+ .optional()
+ .refine((v) => !v || slugify(v) === v, {
+ message: "Slug must be a valid slug"
+ })
+ .describe(ENVIRONMENTS.UPDATE.slug),
+ name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
+ position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
}),
response: {
200: z.object({
@@ -104,6 +115,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
const { environment, old } = await server.services.projectEnv.updateEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
id: req.params.id,
@@ -135,19 +147,21 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/environments/:id",
method: "DELETE",
+ url: "/:workspaceId/environments/:id",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Delete environment",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- workspaceId: z.string().trim(),
- id: z.string().trim()
+ workspaceId: z.string().trim().describe(ENVIRONMENTS.DELETE.workspaceId),
+ id: z.string().trim().describe(ENVIRONMENTS.DELETE.id)
}),
response: {
200: z.object({
@@ -162,6 +176,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
const environment = await server.services.projectEnv.deleteEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
id: req.params.id
diff --git a/backend/src/server/routes/v1/project-key-router.ts b/backend/src/server/routes/v1/project-key-router.ts
index b34260117..bb35794e9 100644
--- a/backend/src/server/routes/v1/project-key-router.ts
+++ b/backend/src/server/routes/v1/project-key-router.ts
@@ -1,5 +1,6 @@
import { z } from "zod";
+import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -7,6 +8,9 @@ export const registerProjectKeyRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:workspaceId/key",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -30,6 +34,7 @@ export const registerProjectKeyRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
nonce: req.body.key.nonce,
receiverId: req.body.key.userId,
diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts
index 999a5d025..d2ed649db 100644
--- a/backend/src/server/routes/v1/project-membership-router.ts
+++ b/backend/src/server/routes/v1/project-membership-router.ts
@@ -9,52 +9,53 @@ import {
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { PROJECTS } 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:workspaceId/memberships",
method: "GET",
+ url: "/:workspaceId/memberships",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Return project user memberships",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.GET_USER_MEMBERSHIPS.workspaceId)
}),
response: {
200: z.object({
- memberships: ProjectMembershipsSchema.omit({ role: true })
- .merge(
+ memberships: ProjectMembershipsSchema.extend({
+ user: UsersSchema.pick({
+ email: true,
+ firstName: true,
+ lastName: true,
+ id: true
+ }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
+ roles: z.array(
z.object({
- user: UsersSchema.pick({
- email: true,
- firstName: true,
- lastName: true,
- id: true
- }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
- roles: z.array(
- z.object({
- id: z.string(),
- role: z.string(),
- customRoleId: z.string().optional().nullable(),
- customRoleName: z.string().optional().nullable(),
- customRoleSlug: z.string().optional().nullable(),
- isTemporary: z.boolean(),
- temporaryMode: z.string().optional().nullable(),
- temporaryRange: z.string().nullable().optional(),
- temporaryAccessStartTime: z.date().nullable().optional(),
- temporaryAccessEndTime: z.date().nullable().optional()
- })
- )
+ id: z.string(),
+ role: z.string(),
+ customRoleId: z.string().optional().nullable(),
+ customRoleName: z.string().optional().nullable(),
+ customRoleSlug: z.string().optional().nullable(),
+ isTemporary: z.boolean(),
+ temporaryMode: z.string().optional().nullable(),
+ temporaryRange: z.string().nullable().optional(),
+ temporaryAccessStartTime: z.date().nullable().optional(),
+ temporaryAccessEndTime: z.date().nullable().optional()
})
)
+ })
.omit({ createdAt: true, updatedAt: true })
.array()
})
@@ -65,6 +66,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
const memberships = await server.services.projectMembership.getProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
@@ -73,8 +75,11 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
});
server.route({
- url: "/:workspaceId/memberships",
method: "POST",
+ url: "/:workspaceId/memberships",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -101,6 +106,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
const data = await server.services.projectMembership.addUsersToProject({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
members: req.body.members
@@ -123,19 +129,21 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
});
server.route({
- url: "/:workspaceId/memberships/:membershipId",
method: "PATCH",
+ url: "/:workspaceId/memberships/:membershipId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Update project user membership",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- workspaceId: z.string().trim(),
- membershipId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.UPDATE_USER_MEMBERSHIP.workspaceId),
+ membershipId: z.string().trim().describe(PROJECTS.UPDATE_USER_MEMBERSHIP.membershipId)
}),
body: z.object({
roles: z
@@ -155,7 +163,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
])
)
.min(1)
- .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least long lived role is required")
+ .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least one long lived role is required")
+ .describe(PROJECTS.UPDATE_USER_MEMBERSHIP.roles)
}),
response: {
200: z.object({
@@ -168,6 +177,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
const roles = await server.services.projectMembership.updateProjectMembership({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
membershipId: req.params.membershipId,
@@ -192,14 +202,16 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
});
server.route({
- url: "/:workspaceId/memberships/:membershipId",
method: "DELETE",
+ url: "/:workspaceId/memberships/:membershipId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Delete project user membership",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
@@ -217,6 +229,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
const membership = await server.services.projectMembership.deleteProjectMembership({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
membershipId: req.params.membershipId
diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts
index dc32702ff..1cf655a97 100644
--- a/backend/src/server/routes/v1/project-router.ts
+++ b/backend/src/server/routes/v1/project-router.ts
@@ -7,8 +7,11 @@ import {
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
+import { PROJECTS } 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 { ProjectFilterType } from "@app/services/project/project-types";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
@@ -22,8 +25,11 @@ const projectWithEnv = ProjectsSchema.merge(
export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:workspaceId/keys",
method: "GET",
+ url: "/:workspaceId/keys",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -44,6 +50,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const publicKeys = await server.services.projectKey.getProjectPublicKeys({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
@@ -52,40 +59,40 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/users",
method: "GET",
+ url: "/:workspaceId/users",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
- users: ProjectMembershipsSchema.omit({ role: true })
- .merge(
+ users: ProjectMembershipsSchema.extend({
+ user: UsersSchema.pick({
+ email: true,
+ username: true,
+ firstName: true,
+ lastName: true,
+ id: true
+ }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
+ roles: z.array(
z.object({
- user: UsersSchema.pick({
- username: true,
- email: true,
- firstName: true,
- lastName: true,
- id: true
- }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
- roles: z.array(
- z.object({
- id: z.string(),
- role: z.string(),
- customRoleId: z.string().optional().nullable(),
- customRoleName: z.string().optional().nullable(),
- customRoleSlug: z.string().optional().nullable(),
- isTemporary: z.boolean(),
- temporaryMode: z.string().optional().nullable(),
- temporaryRange: z.string().nullable().optional(),
- temporaryAccessStartTime: z.date().nullable().optional(),
- temporaryAccessEndTime: z.date().nullable().optional()
- })
- )
+ id: z.string(),
+ role: z.string(),
+ customRoleId: z.string().optional().nullable(),
+ customRoleName: z.string().optional().nullable(),
+ customRoleSlug: z.string().optional().nullable(),
+ isTemporary: z.boolean(),
+ temporaryMode: z.string().optional().nullable(),
+ temporaryRange: z.string().nullable().optional(),
+ temporaryAccessStartTime: z.date().nullable().optional(),
+ temporaryAccessEndTime: z.date().nullable().optional()
})
)
+ })
.omit({ createdAt: true, updatedAt: true })
.array()
})
@@ -96,6 +103,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const users = await server.services.projectMembership.getProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
actorOrgId: req.permission.orgId
});
@@ -104,8 +112,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/",
method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: z.object({
@@ -121,11 +132,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId",
method: "GET",
+ url: "/:workspaceId",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
+ description: "Get project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.GET.workspaceId)
}),
response: {
200: z.object({
@@ -136,48 +156,34 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.getAProject({
+ filter: {
+ type: ProjectFilterType.ID,
+ projectId: req.params.workspaceId
+ },
+ actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
- actorOrgId: req.permission.orgId,
- projectId: req.params.workspaceId
+ actorOrgId: req.permission.orgId
});
return { workspace };
}
});
server.route({
- url: "/",
- method: "POST",
- schema: {
- body: z.object({
- workspaceName: z.string().trim(),
- organizationId: z.string().trim()
- }),
- response: {
- 200: z.object({
- workspace: projectWithEnv
- })
- }
- },
- onRequest: verifyAuth([AuthMode.JWT]),
- handler: async (req) => {
- const workspace = await server.services.project.createProject({
- actorId: req.permission.id,
- actor: req.permission.type,
- orgId: req.body.organizationId,
- actorOrgId: req.permission.orgId,
- workspaceName: req.body.workspaceName
- });
- return { workspace };
- }
- });
-
- server.route({
- url: "/:workspaceId",
method: "DELETE",
+ url: "/:workspaceId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
+ description: "Delete project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.DELETE.workspaceId)
}),
response: {
200: z.object({
@@ -188,10 +194,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.deleteProject({
+ filter: {
+ type: ProjectFilterType.ID,
+ projectId: req.params.workspaceId
+ },
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
- actorOrgId: req.permission.orgId,
- projectId: req.params.workspaceId
+ actorOrgId: req.permission.orgId
});
return { workspace };
}
@@ -200,6 +210,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:workspaceId/name",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -219,6 +232,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const workspace = await server.services.project.updateName({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
name: req.body.name
@@ -231,15 +245,29 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId",
method: "PATCH",
+ url: "/:workspaceId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
+ description: "Update project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.UPDATE.workspaceId)
}),
body: z.object({
- name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
- autoCapitalization: z.boolean().optional()
+ name: z
+ .string()
+ .trim()
+ .max(64, { message: "Name must be 64 or fewer characters" })
+ .optional()
+ .describe(PROJECTS.UPDATE.name),
+ autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
}),
response: {
200: z.object({
@@ -247,17 +275,21 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.updateProject({
- actorId: req.permission.id,
- actor: req.permission.type,
- actorOrgId: req.permission.orgId,
- projectId: req.params.workspaceId,
+ filter: {
+ type: ProjectFilterType.ID,
+ projectId: req.params.workspaceId
+ },
update: {
name: req.body.name,
autoCapitalization: req.body.autoCapitalization
- }
+ },
+ actorAuthMethod: req.permission.authMethod,
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId
});
return {
workspace
@@ -266,8 +298,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/auto-capitalization",
method: "POST",
+ url: "/:workspaceId/auto-capitalization",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -287,6 +322,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const workspace = await server.services.project.toggleAutoCapitalization({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
autoCapitalization: req.body.autoCapitalization
@@ -299,11 +335,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/integrations",
method: "GET",
+ url: "/:workspaceId/integrations",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
+ description: "List integrations for a project.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.workspaceId)
}),
response: {
200: z.object({
@@ -319,10 +364,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integrations = await server.services.integration.listIntegrationByProject({
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
@@ -332,11 +378,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/authorizations",
method: "GET",
+ url: "/:workspaceId/authorizations",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
+ description: "List integration auth objects for a workspace.",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.workspaceId)
}),
response: {
200: z.object({
@@ -344,10 +399,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
@@ -357,8 +413,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:workspaceId/service-token-data",
method: "GET",
+ url: "/:workspaceId/service-token-data",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
workspaceId: z.string().trim()
@@ -373,6 +432,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const serviceTokenData = await server.services.serviceToken.getProjectServiceTokens({
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts
index af1bf7212..3b8d0988f 100644
--- a/backend/src/server/routes/v1/secret-folder-router.ts
+++ b/backend/src/server/routes/v1/secret-folder-router.ts
@@ -2,7 +2,9 @@ import { z } from "zod";
import { SecretFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { FOLDERS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn";
+import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -10,21 +12,23 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
server.route({
url: "/",
method: "POST",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Create folders",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- name: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash),
+ workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId),
+ environment: z.string().trim().describe(FOLDERS.CREATE.environment),
+ name: z.string().trim().describe(FOLDERS.CREATE.name),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.path),
// backward compatiability with cli
- directory: z.string().trim().default("/").transform(removeTrailingSlash)
+ directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.directory)
}),
response: {
200: z.object({
@@ -38,6 +42,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
const folder = await server.services.folder.createFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
@@ -63,25 +68,27 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
server.route({
url: "/:folderId",
method: "PATCH",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Update folder",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
// old way this was name
- folderId: z.string()
+ folderId: z.string().describe(FOLDERS.UPDATE.folderId)
}),
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- name: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash),
+ workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
+ environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
+ name: z.string().trim().describe(FOLDERS.UPDATE.name),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path),
// backward compatiability with cli
- directory: z.string().trim().default("/").transform(removeTrailingSlash)
+ directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.directory)
}),
response: {
200: z.object({
@@ -95,6 +102,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
const { folder, old } = await server.services.folder.updateFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
@@ -119,26 +127,29 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}
});
+ // TODO(daniel): Expose this route in api reference and write docs for it.
server.route({
- url: "/:folderIdOrName",
method: "DELETE",
+ url: "/:folderIdOrName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Delete a folder",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- folderIdOrName: z.string()
+ folderIdOrName: z.string().describe(FOLDERS.DELETE.folderIdOrName)
}),
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash),
+ workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId),
+ environment: z.string().trim().describe(FOLDERS.DELETE.environment),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.path),
// keep this here as cli need directory
- directory: z.string().trim().default("/").transform(removeTrailingSlash)
+ directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.directory)
}),
response: {
200: z.object({
@@ -152,6 +163,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
const folder = await server.services.folder.deleteFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
@@ -176,22 +188,24 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
});
server.route({
- url: "/",
method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Get folders",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
querystring: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash),
+ workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId),
+ environment: z.string().trim().describe(FOLDERS.LIST.environment),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.path),
// backward compatiability with cli
- directory: z.string().trim().default("/").transform(removeTrailingSlash)
+ directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.directory)
}),
response: {
200: z.object({
@@ -205,6 +219,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
const folders = await server.services.folder.getFolders({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId,
diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v1/secret-import-router.ts
index 2ec2d5ce2..d036fdbdd 100644
--- a/backend/src/server/routes/v1/secret-import-router.ts
+++ b/backend/src/server/routes/v1/secret-import-router.ts
@@ -2,29 +2,33 @@ import { z } from "zod";
import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { SECRET_IMPORTS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn";
+import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSecretImportRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/",
method: "POST",
+ url: "/",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Create secret imports",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash),
+ workspaceId: z.string().trim().describe(SECRET_IMPORTS.CREATE.workspaceId),
+ environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.environment),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.path),
import: z.object({
- environment: z.string().trim(),
- path: z.string().trim().transform(removeTrailingSlash)
+ environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
+ path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
})
}),
response: {
@@ -43,6 +47,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
const secretImport = await server.services.secretImport.createImport({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId,
@@ -69,31 +74,34 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
});
server.route({
- url: "/:secretImportId",
method: "PATCH",
+ url: "/:secretImportId",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Update secret imports",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- secretImportId: z.string().trim()
+ secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId)
}),
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash),
+ 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),
import: z.object({
- environment: z.string().trim().optional(),
+ environment: z.string().trim().optional().describe(SECRET_IMPORTS.UPDATE.import.environment),
path: z
.string()
.trim()
.optional()
- .transform((val) => (val ? removeTrailingSlash(val) : val)),
- position: z.number().optional()
+ .transform((val) => (val ? removeTrailingSlash(val) : val))
+ .describe(SECRET_IMPORTS.UPDATE.import.path),
+ position: z.number().optional().describe(SECRET_IMPORTS.UPDATE.import.position)
})
}),
response: {
@@ -112,6 +120,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
const secretImport = await server.services.secretImport.updateImport({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretImportId,
...req.body,
@@ -139,23 +148,25 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
});
server.route({
- url: "/:secretImportId",
method: "DELETE",
+ url: "/:secretImportId",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Delete secret imports",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- secretImportId: z.string().trim()
+ secretImportId: z.string().trim().describe(SECRET_IMPORTS.DELETE.secretImportId)
}),
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash)
+ workspaceId: z.string().trim().describe(SECRET_IMPORTS.DELETE.workspaceId),
+ environment: z.string().trim().describe(SECRET_IMPORTS.DELETE.environment),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.DELETE.path)
}),
response: {
200: z.object({
@@ -173,6 +184,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
const secretImport = await server.services.secretImport.deleteImport({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretImportId,
...req.body,
@@ -199,20 +211,22 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
});
server.route({
- url: "/",
method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Get secret imports",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
querystring: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- path: z.string().trim().default("/").transform(removeTrailingSlash)
+ workspaceId: z.string().trim().describe(SECRET_IMPORTS.LIST.workspaceId),
+ environment: z.string().trim().describe(SECRET_IMPORTS.LIST.environment),
+ path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.LIST.path)
}),
response: {
200: z.object({
@@ -232,6 +246,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
const secretImports = await server.services.secretImport.getImports({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
@@ -256,6 +271,9 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
server.route({
url: "/secrets",
method: "GET",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
@@ -285,6 +303,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
const importedSecrets = await server.services.secretImport.getSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts
index 7ca3e4893..1715aa3c3 100644
--- a/backend/src/server/routes/v1/secret-tag-router.ts
+++ b/backend/src/server/routes/v1/secret-tag-router.ts
@@ -1,16 +1,21 @@
import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas";
+import { SECRET_TAGS } 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";
export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:projectId/tags",
method: "GET",
+ url: "/:projectId/tags",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
- projectId: z.string().trim()
+ projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
}),
response: {
200: z.object({
@@ -23,6 +28,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
const workspaceTags = await server.services.secretTag.getProjectTags({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId
});
@@ -31,16 +37,19 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:projectId/tags",
method: "POST",
+ url: "/:projectId/tags",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
- projectId: z.string().trim()
+ projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}),
body: z.object({
- name: z.string().trim(),
- slug: z.string().trim(),
- color: z.string()
+ name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
+ slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
+ color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}),
response: {
200: z.object({
@@ -53,6 +62,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
const workspaceTag = await server.services.secretTag.createTag({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
...req.body
@@ -62,12 +72,15 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:projectId/tags/:tagId",
method: "DELETE",
+ url: "/:projectId/tags/:tagId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
- projectId: z.string().trim(),
- tagId: z.string().trim()
+ projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
+ tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
}),
response: {
200: z.object({
@@ -80,6 +93,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
const workspaceTag = await server.services.secretTag.deleteTag({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.tagId
});
diff --git a/backend/src/server/routes/v1/user-action-router.ts b/backend/src/server/routes/v1/user-action-router.ts
index c730cdb91..5a2ae484e 100644
--- a/backend/src/server/routes/v1/user-action-router.ts
+++ b/backend/src/server/routes/v1/user-action-router.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { UserActionsSchema } from "@app/db/schemas";
+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";
@@ -8,6 +9,9 @@ export const registerUserActionRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
action: z.string().trim()
@@ -29,6 +33,9 @@ export const registerUserActionRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
querystring: z.object({
action: z.string().trim()
diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts
index ca5148659..bdede8a3a 100644
--- a/backend/src/server/routes/v1/user-router.ts
+++ b/backend/src/server/routes/v1/user-router.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
+import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -8,6 +9,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: z.object({
@@ -15,7 +19,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT]),
+ onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const user = await server.services.user.getMe(req.permission.id);
return { user };
diff --git a/backend/src/server/routes/v1/webhook-router.ts b/backend/src/server/routes/v1/webhook-router.ts
index 9a20a5d22..1698c0c4b 100644
--- a/backend/src/server/routes/v1/webhook-router.ts
+++ b/backend/src/server/routes/v1/webhook-router.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import { WebhooksSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { removeTrailingSlash } from "@app/lib/fn";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -27,6 +28,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
@@ -47,6 +51,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
const webhook = await server.services.webhook.createWebhook({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...req.body
@@ -74,6 +79,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
server.route({
method: "PATCH",
url: "/:webhookId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -93,6 +101,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
const webhook = await server.services.webhook.updateWebhook({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.webhookId,
isDisabled: req.body.isDisabled
@@ -120,6 +129,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:webhookId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -130,6 +142,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
const webhook = await server.services.webhook.deleteWebhook({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.webhookId
});
@@ -156,6 +169,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:webhookId/test",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -172,6 +188,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
const webhook = await server.services.webhook.testWebhook({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.webhookId
});
@@ -182,6 +199,9 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
@@ -204,6 +224,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
const webhooks = await server.services.webhook.listWebhooks({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query,
projectId: req.query.workspaceId
diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts
new file mode 100644
index 000000000..6d438c1ff
--- /dev/null
+++ b/backend/src/server/routes/v2/group-project-router.ts
@@ -0,0 +1,201 @@
+import ms from "ms";
+import { z } from "zod";
+
+import {
+ GroupProjectMembershipsSchema,
+ GroupsSchema,
+ ProjectMembershipRole,
+ ProjectUserMembershipRolesSchema
+} from "@app/db/schemas";
+import { PROJECTS } from "@app/lib/api-docs";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AuthMode } from "@app/services/auth/auth-type";
+import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
+
+export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
+ server.route({
+ method: "POST",
+ url: "/:projectSlug/groups/:groupSlug",
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Add group to project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug),
+ groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug)
+ }),
+ body: z.object({
+ role: z
+ .string()
+ .trim()
+ .min(1)
+ .default(ProjectMembershipRole.NoAccess)
+ .describe(PROJECTS.ADD_GROUP_TO_PROJECT.role)
+ }),
+ response: {
+ 200: z.object({
+ groupMembership: GroupProjectMembershipsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const groupMembership = await server.services.groupProject.addGroupToProject({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ groupSlug: req.params.groupSlug,
+ projectSlug: req.params.projectSlug,
+ role: req.body.role
+ });
+ return { groupMembership };
+ }
+ });
+
+ server.route({
+ method: "PATCH",
+ url: "/:projectSlug/groups/:groupSlug",
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Update group in project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ projectSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectSlug),
+ groupSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupSlug)
+ }),
+ body: z.object({
+ roles: z
+ .array(
+ z.union([
+ z.object({
+ role: z.string(),
+ isTemporary: z.literal(false).default(false)
+ }),
+ z.object({
+ role: z.string(),
+ isTemporary: z.literal(true),
+ temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
+ temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
+ temporaryAccessStartTime: z.string().datetime()
+ })
+ ])
+ )
+ .min(1)
+ .describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.roles)
+ }),
+ response: {
+ 200: z.object({
+ roles: ProjectUserMembershipRolesSchema.array()
+ })
+ }
+ },
+ handler: async (req) => {
+ const roles = await server.services.groupProject.updateGroupInProject({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ groupSlug: req.params.groupSlug,
+ projectSlug: req.params.projectSlug,
+ roles: req.body.roles
+ });
+ return { roles };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/:projectSlug/groups/:groupSlug",
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Remove group from project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ projectSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectSlug),
+ groupSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupSlug)
+ }),
+ response: {
+ 200: z.object({
+ groupMembership: GroupProjectMembershipsSchema
+ })
+ }
+ },
+ handler: async (req) => {
+ const groupMembership = await server.services.groupProject.removeGroupFromProject({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ groupSlug: req.params.groupSlug,
+ projectSlug: req.params.projectSlug
+ });
+ return { groupMembership };
+ }
+ });
+
+ server.route({
+ method: "GET",
+ url: "/:projectSlug/groups",
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ schema: {
+ description: "Return list of groups in project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ projectSlug: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectSlug)
+ }),
+ response: {
+ 200: z.object({
+ groupMemberships: z
+ .object({
+ id: z.string(),
+ groupId: z.string(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+ roles: z.array(
+ z.object({
+ id: z.string(),
+ role: z.string(),
+ customRoleId: z.string().optional().nullable(),
+ customRoleName: z.string().optional().nullable(),
+ customRoleSlug: z.string().optional().nullable(),
+ isTemporary: z.boolean(),
+ temporaryMode: z.string().optional().nullable(),
+ temporaryRange: z.string().nullable().optional(),
+ temporaryAccessStartTime: z.date().nullable().optional(),
+ temporaryAccessEndTime: z.date().nullable().optional()
+ })
+ ),
+ group: GroupsSchema.pick({ name: true, id: true, slug: true })
+ })
+ .array()
+ })
+ }
+ },
+ handler: async (req) => {
+ const groupMemberships = await server.services.groupProject.listGroupsInProject({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ projectSlug: req.params.projectSlug
+ });
+ return { groupMemberships };
+ }
+ });
+};
diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts
index 1832e8962..aab84ef8d 100644
--- a/backend/src/server/routes/v2/identity-org-router.ts
+++ b/backend/src/server/routes/v2/identity-org-router.ts
@@ -1,6 +1,8 @@
import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
+import { ORGANIZATIONS } 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";
@@ -8,17 +10,19 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:orgId/identity-memberships",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Return organization identity memberships",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- orgId: z.string().trim()
+ orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId)
}),
response: {
200: z.object({
@@ -41,9 +45,11 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
const identityMemberships = await server.services.identity.listOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.params.orgId
});
+
return { identityMemberships };
}
});
diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts
index 09e586839..a4068053f 100644
--- a/backend/src/server/routes/v2/identity-project-router.ts
+++ b/backend/src/server/routes/v2/identity-project-router.ts
@@ -7,6 +7,8 @@ import {
ProjectMembershipRole,
ProjectUserMembershipRolesSchema
} from "@app/db/schemas";
+import { PROJECTS } 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
@@ -15,6 +17,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
server.route({
method: "POST",
url: "/:projectId/identity-memberships/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
params: z.object({
@@ -34,6 +39,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
const identityMembership = await server.services.identityProject.createProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId,
@@ -46,6 +52,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
server.route({
method: "PATCH",
url: "/:projectId/identity-memberships/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update project identity memberships",
@@ -55,8 +64,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}
],
params: z.object({
- projectId: z.string().trim(),
- identityId: z.string().trim()
+ projectId: z.string().trim().describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.projectId),
+ identityId: z.string().trim().describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
@@ -76,6 +85,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
])
)
.min(1)
+ .describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.roles)
}),
response: {
200: z.object({
@@ -87,6 +97,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
const roles = await server.services.identityProject.updateProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId,
@@ -99,6 +110,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
server.route({
method: "DELETE",
url: "/:projectId/identity-memberships/:identityId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete project identity memberships",
@@ -108,8 +122,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}
],
params: z.object({
- projectId: z.string().trim(),
- identityId: z.string().trim()
+ projectId: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.projectId),
+ identityId: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
@@ -121,6 +135,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
const identityMembership = await server.services.identityProject.deleteProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId
@@ -132,6 +147,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
server.route({
method: "GET",
url: "/:projectId/identity-memberships",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Return project identity memberships",
@@ -141,7 +159,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}
],
params: z.object({
- projectId: z.string().trim()
+ projectId: z.string().trim().describe(PROJECTS.LIST_IDENTITY_MEMBERSHIPS.projectId)
}),
response: {
200: z.object({
@@ -175,6 +193,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
const identityMemberships = await server.services.identityProject.listProjectIdentities({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId
});
diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts
index deebbc981..3d7581a70 100644
--- a/backend/src/server/routes/v2/index.ts
+++ b/backend/src/server/routes/v2/index.ts
@@ -1,3 +1,4 @@
+import { registerGroupProjectRouter } from "./group-project-router";
import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router";
@@ -22,6 +23,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
async (projectServer) => {
await projectServer.register(registerProjectRouter);
await projectServer.register(registerIdentityProjectRouter);
+ await projectServer.register(registerGroupProjectRouter);
await projectServer.register(registerProjectMembershipRouter);
},
{ prefix: "/workspace" }
diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts
index 2c9465aa8..973804c7c 100644
--- a/backend/src/server/routes/v2/mfa-router.ts
+++ b/backend/src/server/routes/v2/mfa-router.ts
@@ -2,6 +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 { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
export const registerMfaRouter = async (server: FastifyZodProvider) => {
@@ -30,8 +31,11 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/mfa/send",
method: "POST",
+ url: "/mfa/send",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
response: {
200: z.object({
@@ -48,6 +52,9 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/mfa/verify",
method: "POST",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
mfaToken: z.string().trim()
@@ -68,11 +75,14 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
+ const mfaJwtToken = req.headers.authorization?.replace("Bearer ", "");
if (!userAgent) throw new Error("user agent header is required");
+ if (!mfaJwtToken) throw new Error("authorization header is required");
const appCfg = getConfig();
const { user, token } = await server.services.login.verifyMfaToken({
userAgent,
+ mfaJwtToken,
ip: req.realIp,
userId: req.mfa.userId,
orgId: req.mfa.orgId,
diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts
index ba5ca3c21..4a4e8b15b 100644
--- a/backend/src/server/routes/v2/organization-router.ts
+++ b/backend/src/server/routes/v2/organization-router.ts
@@ -1,6 +1,8 @@
import { z } from "zod";
import { OrganizationsSchema, OrgMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
+import { ORGANIZATIONS } from "@app/lib/api-docs";
+import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -8,16 +10,18 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/memberships",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Return organization user memberships",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- organizationId: z.string().trim()
+ organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_USER_MEMBERSHIPS.organizationId)
}),
response: {
200: z.object({
@@ -44,6 +48,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const users = await server.services.org.findAllOrgMembers(
req.permission.id,
req.params.organizationId,
+ req.permission.authMethod,
req.permission.orgId
);
return { users };
@@ -53,16 +58,18 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/workspaces",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Return projects in organization that user is part of",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- organizationId: z.string().trim()
+ organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId)
}),
response: {
200: z.object({
@@ -88,6 +95,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
});
@@ -98,17 +106,22 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "PATCH",
url: "/:organizationId/memberships/:membershipId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Update organization user memberships",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
- params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
+ params: z.object({
+ organizationId: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.organizationId),
+ membershipId: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.membershipId)
+ }),
body: z.object({
- role: z.string().trim()
+ role: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role)
}),
response: {
200: z.object({
@@ -123,6 +136,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const membership = await server.services.org.updateOrgMembership({
userId: req.permission.id,
role: req.body.role,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
membershipId: req.params.membershipId,
actorOrgId: req.permission.orgId
@@ -134,15 +148,20 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:organizationId/memberships/:membershipId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
description: "Delete organization user memberships",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
- params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
+ params: z.object({
+ organizationId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.organizationId),
+ membershipId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.membershipId)
+ }),
response: {
200: z.object({
membership: OrgMembershipsSchema
@@ -155,6 +174,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const membership = await server.services.org.deleteOrgMembership({
userId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
membershipId: req.params.membershipId,
actorOrgId: req.permission.orgId
@@ -166,6 +186,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
+ config: {
+ rateLimit: creationLimit
+ },
schema: {
body: z.object({
name: z.string().trim()
@@ -176,7 +199,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY], { requireOrg: false }),
handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return;
@@ -193,6 +216,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:organizationId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
organizationId: z.string().trim()
@@ -210,6 +236,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const organization = await server.services.org.deleteOrganizationById(
req.permission.id,
req.params.organizationId,
+ req.permission.authMethod,
req.permission.orgId
);
return { organization };
diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts
index f63770346..96471dc2c 100644
--- a/backend/src/server/routes/v2/project-membership-router.ts
+++ b/backend/src/server/routes/v2/project-membership-router.ts
@@ -2,6 +2,8 @@ import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
+import { PROJECTS } from "@app/lib/api-docs";
+import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -9,13 +11,22 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
server.route({
method: "POST",
url: "/:projectId/memberships",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
+ description: "Invite members to project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- projectId: z.string().describe("The ID of the project.")
+ projectId: z.string().describe(PROJECTS.INVITE_MEMBER.projectId)
}),
body: z.object({
- emails: z.string().email().array().default([]).describe("Emails of the users to add to the project."),
- usernames: z.string().array().default([]).describe("Usernames of the users to add to the project.")
+ emails: z.string().email().array().default([]).describe(PROJECTS.INVITE_MEMBER.emails),
+ usernames: z.string().array().default([]).describe(PROJECTS.INVITE_MEMBER.usernames)
}),
response: {
200: z.object({
@@ -27,7 +38,9 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
handler: async (req) => {
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
projectId: req.params.projectId,
+ actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
actor: req.permission.type,
emails: req.body.emails,
usernames: req.body.usernames
@@ -53,14 +66,22 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
server.route({
method: "DELETE",
url: "/:projectId/memberships",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
+ description: "Remove members from project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
params: z.object({
- projectId: z.string().describe("The ID of the project.")
+ projectId: z.string().describe(PROJECTS.REMOVE_MEMBER.projectId)
}),
-
body: z.object({
- emails: z.string().email().array().default([]).describe("Emails of the users to remove from the project."),
- usernames: z.string().array().default([]).describe("Usernames of the users to remove from the project.")
+ emails: z.string().email().array().default([]).describe(PROJECTS.REMOVE_MEMBER.emails),
+ usernames: z.string().array().default([]).describe(PROJECTS.REMOVE_MEMBER.usernames)
}),
response: {
200: z.object({
@@ -73,6 +94,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
const memberships = await server.services.projectMembership.deleteProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
emails: req.body.emails,
diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts
index fe1254b2b..a199cf0d4 100644
--- a/backend/src/server/routes/v2/project-router.ts
+++ b/backend/src/server/routes/v2/project-router.ts
@@ -3,10 +3,12 @@ import { z } from "zod";
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
-import { authRateLimit } from "@app/server/config/rateLimiter";
+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 { ProjectFilterType } from "@app/services/project/project-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
const projectWithEnv = ProjectsSchema.merge(
@@ -16,20 +18,26 @@ const projectWithEnv = ProjectsSchema.merge(
})
);
+const slugSchema = z
+ .string()
+ .min(5)
+ .max(36)
+ .refine((v) => slugify(v) === v, {
+ message: "Slug must be at least 5 character but no more than 36"
+ });
+
export const registerProjectRouter = async (server: FastifyZodProvider) => {
/* Get project key */
server.route({
- url: "/:workspaceId/encrypted-key",
method: "GET",
+ url: "/:workspaceId/encrypted-key",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Return encrypted project key",
- security: [
- {
- apiKeyAuth: []
- }
- ],
params: z.object({
- workspaceId: z.string().trim()
+ workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.workspaceId)
}),
response: {
200: ProjectKeysSchema.merge(
@@ -46,6 +54,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const key = await server.services.projectKey.getLatestProjectKey({
actor: req.permission.type,
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
@@ -67,13 +76,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
/* Start upgrade of a project */
server.route({
- url: "/:projectId/upgrade",
method: "POST",
+ url: "/:projectId/upgrade",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
}),
-
body: z.object({
userPrivateKey: z.string().trim()
}),
@@ -81,11 +92,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.void()
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
+ onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.project.upgradeProject({
actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
projectId: req.params.projectId,
userPrivateKey: req.body.userPrivateKey
});
@@ -96,6 +109,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:projectId/upgrade/status",
method: "GET",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -106,9 +122,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
+ onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const status = await server.services.project.getProjectUpgradeStatus({
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
actor: req.permission.type,
actorId: req.permission.id
@@ -123,11 +141,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
- rateLimit: authRateLimit
+ rateLimit: creationLimit
},
schema: {
+ description: "Create a new project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
body: z.object({
- projectName: z.string().trim(),
+ projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
slug: z
.string()
.min(5)
@@ -135,8 +159,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
- .optional(),
- organizationId: z.string().trim()
+ .optional()
+ .describe(PROJECTS.CREATE.slug)
}),
response: {
200: z.object({
@@ -144,12 +168,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const project = await server.services.project.createProject({
actorId: req.permission.id,
actor: req.permission.type,
- orgId: req.body.organizationId,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName,
slug: req.body.slug
});
@@ -158,7 +183,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
event: PostHogEventTypes.ProjectCreated,
distinctId: getTelemetryDistinctId(req),
properties: {
- orgId: req.body.organizationId,
+ orgId: project.orgId,
name: project.name,
...req.auditLogInfo
}
@@ -167,4 +192,119 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return { project };
}
});
+
+ /* Delete a project by slug */
+ server.route({
+ method: "DELETE",
+ url: "/:slug",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Delete project",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ slug: slugSchema.describe("The slug of the project to delete.")
+ }),
+ response: {
+ 200: ProjectsSchema
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+
+ handler: async (req) => {
+ const project = await server.services.project.deleteProject({
+ filter: {
+ type: ProjectFilterType.SLUG,
+ slug: req.params.slug,
+ orgId: req.permission.orgId
+ },
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ actor: req.permission.type
+ });
+
+ return project;
+ }
+ });
+
+ /* Get a project by slug */
+ server.route({
+ method: "GET",
+ url: "/:slug",
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ slug: slugSchema.describe("The slug of the project to get.")
+ }),
+ response: {
+ 200: projectWithEnv
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const project = await server.services.project.getAProject({
+ 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
+ });
+
+ return project;
+ }
+ });
+
+ /* Update a project by slug */
+ server.route({
+ method: "PATCH",
+ url: "/:slug",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ params: z.object({
+ slug: slugSchema.describe("The slug of the project to update.")
+ }),
+ body: z.object({
+ name: z.string().trim().optional().describe("The new name of the project."),
+ autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.")
+ }),
+ response: {
+ 200: ProjectsSchema
+ }
+ },
+
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const project = await server.services.project.updateProject({
+ filter: {
+ type: ProjectFilterType.SLUG,
+ slug: req.params.slug,
+ orgId: req.permission.orgId
+ },
+ update: {
+ name: req.body.name,
+ autoCapitalization: req.body.autoCapitalization
+ },
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId
+ });
+
+ return project;
+ }
+ });
};
diff --git a/backend/src/server/routes/v2/service-token-router.ts b/backend/src/server/routes/v2/service-token-router.ts
index 2b6445dea..fb10f17db 100644
--- a/backend/src/server/routes/v2/service-token-router.ts
+++ b/backend/src/server/routes/v2/service-token-router.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import { ServiceTokensSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { removeTrailingSlash } from "@app/lib/fn";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -17,8 +18,11 @@ export const sanitizedServiceTokenSchema = ServiceTokensSchema.omit({
export const registerServiceTokenRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/",
method: "GET",
+ url: "/",
+ config: {
+ rateLimit: readLimit
+ },
onRequest: verifyAuth([AuthMode.SERVICE_TOKEN]),
schema: {
description: "Return Infisical Token data",
@@ -46,6 +50,8 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
handler: async (req) => {
const { serviceToken, user } = await server.services.serviceToken.getServiceToken({
actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
actor: req.permission.type
});
@@ -67,8 +73,11 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
});
server.route({
- url: "/",
method: "POST",
+ url: "/",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
@@ -98,6 +107,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
const { serviceToken, token } = await server.services.serviceToken.createServiceToken({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
projectId: req.body.workspaceId
@@ -119,8 +129,11 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
});
server.route({
- url: "/:serviceTokenId",
method: "DELETE",
+ url: "/:serviceTokenId",
+ config: {
+ rateLimit: writeLimit
+ },
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
@@ -136,6 +149,7 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
const serviceTokenData = await server.services.serviceToken.deleteServiceToken({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.serviceTokenId
});
diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts
index 97bc3d864..d1e80702f 100644
--- a/backend/src/server/routes/v2/user-router.ts
+++ b/backend/src/server/routes/v2/user-router.ts
@@ -2,13 +2,17 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/me/mfa",
method: "PATCH",
+ url: "/me/mfa",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
isMfaEnabled: z.boolean()
@@ -27,8 +31,11 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/me/name",
method: "PATCH",
+ url: "/me/name",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
firstName: z.string().trim(),
@@ -48,8 +55,11 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/me/auth-methods",
method: "PUT",
+ url: "/me/auth-methods",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
authMethods: z.nativeEnum(AuthMethod).array().min(1)
@@ -60,7 +70,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
})
}
},
- preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
+ preHandler: verifyAuth([AuthMode.JWT, AuthMode.API_KEY], { requireOrg: false }),
handler: async (req) => {
const user = await server.services.user.updateAuthMethods(req.permission.id, req.body.authMethods);
return { user };
@@ -70,13 +80,11 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/me/organizations",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Return organizations that current user is part of",
- security: [
- {
- apiKeyAuth: []
- }
- ],
response: {
200: z.object({
organizations: OrganizationsSchema.array()
@@ -93,6 +101,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/me/api-keys",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: ApiKeysSchema.omit({ secretHash: true }).array()
@@ -108,6 +119,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/me/api-keys",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
body: z.object({
name: z.string().trim(),
@@ -130,6 +144,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/me/api-keys/:apiKeyDataId",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
apiKeyDataId: z.string().trim()
@@ -150,6 +167,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/me/sessions",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: AuthTokenSessionsSchema.array()
@@ -165,6 +185,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/me/sessions",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
response: {
200: z.object({
@@ -184,13 +207,11 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/me",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
description: "Retrieve the current user on the request",
- security: [
- {
- apiKeyAuth: []
- }
- ],
response: {
200: z.object({
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
@@ -207,6 +228,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/me",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
response: {
200: z.object({
diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts
index 240aa21b1..900ad56d2 100644
--- a/backend/src/server/routes/v3/login-router.ts
+++ b/backend/src/server/routes/v3/login-router.ts
@@ -34,6 +34,42 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ method: "POST",
+ url: "/select-organization",
+ config: {
+ rateLimit: authRateLimit
+ },
+ schema: {
+ body: z.object({
+ organizationId: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ token: z.string()
+ })
+ }
+ },
+ handler: async (req, res) => {
+ const cfg = getConfig();
+ const tokens = await server.services.login.selectOrganization({
+ userAgent: req.headers["user-agent"],
+ authJwtToken: req.headers.authorization,
+ organizationId: req.body.organizationId,
+ ipAddress: req.realIp
+ });
+
+ void res.setCookie("jid", tokens.refresh, {
+ httpOnly: true,
+ path: "/",
+ sameSite: "strict",
+ secure: cfg.HTTPS_ENABLED
+ });
+
+ return { token: tokens.access };
+ }
+ });
+
server.route({
method: "POST",
url: "/login2",
diff --git a/backend/src/server/routes/v3/secret-blind-index-router.ts b/backend/src/server/routes/v3/secret-blind-index-router.ts
index 94e6cab83..cfb27a58e 100644
--- a/backend/src/server/routes/v3/secret-blind-index-router.ts
+++ b/backend/src/server/routes/v3/secret-blind-index-router.ts
@@ -1,13 +1,17 @@
import { z } from "zod";
import { SecretsSchema } from "@app/db/schemas";
+import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/:projectId/secrets/blind-index-status",
method: "GET",
+ url: "/:projectId/secrets/blind-index-status",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -20,6 +24,7 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider)
handler: async (req) => {
const count = await server.services.secretBlindIndex.getSecretBlindIndexStatus({
projectId: req.params.projectId,
+ actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
@@ -29,8 +34,11 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:projectId/secrets",
method: "GET",
+ url: "/:projectId/secrets",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -52,6 +60,7 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider)
handler: async (req) => {
const secrets = await server.services.secretBlindIndex.getProjectSecrets({
projectId: req.params.projectId,
+ actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
@@ -61,8 +70,11 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider)
});
server.route({
- url: "/:projectId/secrets/names",
method: "POST",
+ url: "/:projectId/secrets/names",
+ config: {
+ rateLimit: writeLimit
+ },
schema: {
params: z.object({
projectId: z.string().trim()
@@ -86,6 +98,7 @@ export const registerSecretBlindIndexRouter = async (server: FastifyZodProvider)
await server.services.secretBlindIndex.updateProjectSecretName({
projectId: req.params.projectId,
secretsToUpdate: req.body.secretsToUpdate,
+ actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId
diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts
index 6b3dd6041..b1d852a88 100644
--- a/backend/src/server/routes/v3/secret-router.ts
+++ b/backend/src/server/routes/v3/secret-router.ts
@@ -10,40 +10,180 @@ import {
} 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 { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
+import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
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 { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
- url: "/raw",
+ method: "POST",
+ url: "/tags/:secretName",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Attach tags to a secret",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ secretName: z.string().trim().describe(SECRETS.ATTACH_TAGS.secretName)
+ }),
+ body: z.object({
+ projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
+ environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment),
+ secretPath: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(SECRETS.ATTACH_TAGS.secretPath),
+ type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.ATTACH_TAGS.type),
+ tagSlugs: z.string().array().min(1).describe(SECRETS.ATTACH_TAGS.tagSlugs)
+ }),
+ response: {
+ 200: z.object({
+ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
+ z.object({
+ tags: SecretTagsSchema.pick({
+ id: true,
+ slug: true,
+ name: true,
+ color: true
+ }).array()
+ })
+ )
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const secret = await server.services.secret.attachTags({
+ secretName: req.params.secretName,
+ tagSlugs: req.body.tagSlugs,
+ path: req.body.secretPath,
+ environment: req.body.environment,
+ type: req.body.type,
+ projectSlug: req.body.projectSlug,
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ return { secret };
+ }
+ });
+
+ server.route({
+ method: "DELETE",
+ url: "/tags/:secretName",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Detach tags from a secret",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ secretName: z.string().trim().describe(SECRETS.DETACH_TAGS.secretName)
+ }),
+ body: z.object({
+ projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug),
+ environment: z.string().trim().describe(SECRETS.DETACH_TAGS.environment),
+ secretPath: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(SECRETS.DETACH_TAGS.secretPath),
+ type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.DETACH_TAGS.type),
+ tagSlugs: z.string().array().min(1).describe(SECRETS.DETACH_TAGS.tagSlugs)
+ }),
+ response: {
+ 200: z.object({
+ secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
+ z.object({
+ tags: SecretTagsSchema.pick({
+ id: true,
+ slug: true,
+ name: true,
+ color: true
+ }).array()
+ })
+ )
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const secret = await server.services.secret.detachTags({
+ secretName: req.params.secretName,
+ tagSlugs: req.body.tagSlugs,
+ path: req.body.secretPath,
+ environment: req.body.environment,
+ type: req.body.type,
+ projectSlug: req.body.projectSlug,
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId
+ });
+
+ return { secret };
+ }
+ });
+
+ server.route({
method: "GET",
+ url: "/raw",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "List secrets",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
querystring: z.object({
- workspaceId: z.string().trim().optional(),
- environment: z.string().trim().optional(),
- secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
+ workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId),
+ workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
+ environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
+ secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
+ recursive: z
+ .enum(["true", "false"])
+ .default("false")
+ .transform((value) => value === "true")
+ .describe(RAW_SECRETS.LIST.recursive),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
+ .describe(RAW_SECRETS.LIST.includeImports)
}),
response: {
200: z.object({
- secrets: secretRawSchema.array(),
+ secrets: secretRawSchema
+ .extend({
+ secretPath: z.string().optional()
+ })
+ .array(),
imports: z
.object({
secretPath: z.string(),
@@ -68,6 +208,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment = scope[0].environment;
workspaceId = req.auth.serviceToken.projectId;
}
+ } else if (req.permission.type === ActorType.IDENTITY && req.query.workspaceSlug && !workspaceId) {
+ const workspace = await server.services.project.getAProject({
+ filter: {
+ type: ProjectFilterType.SLUG,
+ orgId: req.permission.orgId,
+ slug: req.query.workspaceSlug
+ },
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId
+ });
+
+ if (!workspace) throw new BadRequestError({ message: `No project found with slug ${req.query.workspaceSlug}` });
+
+ workspaceId = workspace.id;
}
if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
@@ -77,13 +233,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
+ actorAuthMethod: req.permission.authMethod,
projectId: workspaceId,
path: secretPath,
- includeImports: req.query.include_imports
+ includeImports: req.query.include_imports,
+ recursive: req.query.recursive
});
await server.services.auditLog.createAuditLog({
- projectId: req.query.workspaceId,
+ projectId: workspaceId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
@@ -112,29 +270,32 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/raw/:secretName",
method: "GET",
+ url: "/raw/:secretName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Get a secret by name",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- secretName: z.string().trim()
+ secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName)
}),
querystring: z.object({
- workspaceId: z.string().trim().optional(),
- environment: z.string().trim().optional(),
- secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
- version: z.coerce.number().optional(),
- type: z.nativeEnum(SecretType).default(SecretType.Shared),
+ workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId),
+ environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment),
+ secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
+ version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
+ type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
+ .describe(RAW_SECRETS.GET.includeImports)
}),
response: {
200: z.object({
@@ -160,6 +321,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secret = await server.services.secret.getSecretByNameRaw({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment,
projectId: workspaceId,
@@ -202,27 +364,37 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/raw/:secretName",
method: "POST",
+ url: "/raw/:secretName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Create secret",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- secretName: z.string().trim()
+ secretName: z.string().trim().describe(RAW_SECRETS.CREATE.secretName)
}),
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
- secretValue: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
- secretComment: z.string().trim().optional().default(""),
- skipMultilineEncoding: z.boolean().optional(),
- type: z.nativeEnum(SecretType).default(SecretType.Shared)
+ workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId),
+ environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
+ secretPath: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(RAW_SECRETS.CREATE.secretPath),
+ secretValue: z
+ .string()
+ .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),
+ skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
+ type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type)
}),
response: {
200: z.object({
@@ -237,6 +409,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment: req.body.environment,
+ actorAuthMethod: req.permission.authMethod,
projectId: req.body.workspaceId,
secretPath: req.body.secretPath,
secretName: req.params.secretName,
@@ -279,26 +452,36 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/raw/:secretName",
method: "PATCH",
+ url: "/raw/:secretName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Update secret",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- secretName: z.string().trim()
+ secretName: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName)
}),
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- secretValue: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
- secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
- skipMultilineEncoding: z.boolean().optional(),
- type: z.nativeEnum(SecretType).default(SecretType.Shared)
+ workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId),
+ environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
+ secretValue: z
+ .string()
+ .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
+ .describe(RAW_SECRETS.UPDATE.secretValue),
+ secretPath: z
+ .string()
+ .trim()
+ .default("/")
+ .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)
}),
response: {
200: z.object({
@@ -312,6 +495,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
environment: req.body.environment,
projectId: req.body.workspaceId,
secretPath: req.body.secretPath,
@@ -353,24 +537,31 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/raw/:secretName",
method: "DELETE",
+ url: "/raw/:secretName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
description: "Delete secret",
security: [
{
- bearerAuth: [],
- apiKeyAuth: []
+ bearerAuth: []
}
],
params: z.object({
- secretName: z.string().trim()
+ secretName: z.string().trim().describe(RAW_SECRETS.DELETE.secretName)
}),
body: z.object({
- workspaceId: z.string().trim(),
- environment: z.string().trim(),
- secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
- type: z.nativeEnum(SecretType).default(SecretType.Shared)
+ workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId),
+ environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
+ secretPath: z
+ .string()
+ .trim()
+ .default("/")
+ .transform(removeTrailingSlash)
+ .describe(RAW_SECRETS.DELETE.secretPath),
+ type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.DELETE.type)
}),
response: {
200: z.object({
@@ -383,6 +574,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secret = await server.services.secret.deleteSecretRaw({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment: req.body.environment,
projectId: req.body.workspaceId,
@@ -424,13 +616,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/",
method: "GET",
+ url: "/",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
querystring: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
+ recursive: z
+ .enum(["true", "false"])
+ .default("false")
+ .transform((value) => value === "true"),
include_imports: z
.enum(["true", "false"])
.default("false")
@@ -439,19 +638,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true })
- .merge(
- z.object({
- _id: z.string(),
- workspace: z.string(),
- environment: z.string(),
- tags: SecretTagsSchema.pick({
- id: true,
- slug: true,
- name: true,
- color: true
- }).array()
- })
- )
+ .extend({
+ _id: z.string(),
+ workspace: z.string(),
+ environment: z.string(),
+ secretPath: z.string().optional(),
+ tags: SecretTagsSchema.pick({
+ id: true,
+ slug: true,
+ name: true,
+ color: true
+ }).array()
+ })
.array(),
imports: z
.object({
@@ -478,11 +676,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secrets, imports } = await server.services.secret.getSecrets({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment: req.query.environment,
projectId: req.query.workspaceId,
path: req.query.secretPath,
- includeImports: req.query.include_imports
+ includeImports: req.query.include_imports,
+ recursive: req.query.recursive
});
await server.services.auditLog.createAuditLog({
@@ -531,8 +731,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:secretName",
method: "GET",
+ url: "/:secretName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
params: z.object({
secretName: z.string().trim()
@@ -564,6 +767,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secret = await server.services.secret.getSecretByName({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment: req.query.environment,
projectId: req.query.workspaceId,
@@ -608,6 +812,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:secretName",
method: "POST",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
body: z.object({
workspaceId: z.string().trim(),
@@ -666,6 +873,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
if (req.body.type !== SecretType.Personal && req.permission.type === ActorType.USER) {
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actorId: req.permission.id,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
secretPath,
environment,
@@ -675,6 +884,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -718,6 +928,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secret = await server.services.secret.createSecret({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
path: secretPath,
type,
@@ -770,8 +981,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:secretName",
method: "PATCH",
+ url: "/:secretName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
params: z.object({
secretName: z.string()
@@ -842,6 +1056,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -851,6 +1066,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -896,6 +1112,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secret = await server.services.secret.updateSecret({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
path: secretPath,
type,
@@ -951,8 +1168,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/:secretName",
method: "DELETE",
+ url: "/:secretName",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
params: z.object({
secretName: z.string()
@@ -986,6 +1206,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -995,6 +1216,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -1028,6 +1250,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secret = await server.services.secret.deleteSecret({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
path: secretPath,
type,
@@ -1069,8 +1292,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/batch",
method: "POST",
+ url: "/batch",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
body: z.object({
workspaceId: z.string().trim(),
@@ -1110,6 +1336,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -1119,6 +1346,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -1148,6 +1376,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secrets = await server.services.secret.createManySecret({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
path: secretPath,
environment,
@@ -1189,8 +1418,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/batch",
method: "PATCH",
+ url: "/batch",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
body: z.object({
workspaceId: z.string().trim(),
@@ -1231,6 +1463,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -1240,6 +1473,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -1268,6 +1502,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secrets = await server.services.secret.updateManySecret({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
path: secretPath,
environment,
@@ -1309,8 +1544,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
server.route({
- url: "/batch",
method: "DELETE",
+ url: "/batch",
+ config: {
+ rateLimit: secretsLimit
+ },
schema: {
body: z.object({
workspaceId: z.string().trim(),
@@ -1340,6 +1578,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -1349,6 +1588,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const approval = await server.services.secretApprovalRequest.generateSecretApprovalRequest({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
@@ -1376,6 +1616,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const secrets = await server.services.secret.deleteManySecret({
actorId: req.permission.id,
actor: req.permission.type,
+ actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
path: req.body.secretPath,
environment,
diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts
index 17787be84..ac43df36d 100644
--- a/backend/src/server/routes/v3/signup-router.ts
+++ b/backend/src/server/routes/v3/signup-router.ts
@@ -108,7 +108,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
200: z.object({
message: z.string(),
user: UsersSchema,
- token: z.string()
+ token: z.string(),
+ organizationId: z.string().nullish()
})
}
},
@@ -124,12 +125,13 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
});
}
- const { user, accessToken, refreshToken } = await server.services.signup.completeEmailAccountSignup({
- ...req.body,
- ip: req.realIp,
- userAgent,
- authorization: req.headers.authorization as string
- });
+ const { user, accessToken, refreshToken, organizationId } =
+ await server.services.signup.completeEmailAccountSignup({
+ ...req.body,
+ ip: req.realIp,
+ userAgent,
+ authorization: req.headers.authorization as string
+ });
if (user.email) {
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || "");
@@ -152,7 +154,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
- return { message: "Successfully set up account", user, token: accessToken };
+ return { message: "Successfully set up account", user, token: accessToken, organizationId };
}
});
diff --git a/backend/src/server/routes/v3/user-router.ts b/backend/src/server/routes/v3/user-router.ts
index 1672405b1..a9fdba358 100644
--- a/backend/src/server/routes/v3/user-router.ts
+++ b/backend/src/server/routes/v3/user-router.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
+import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -8,6 +9,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/me/api-keys",
+ config: {
+ rateLimit: readLimit
+ },
schema: {
response: {
200: z.object({
diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts
index 0b78ab438..80fb0b325 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 };
+ return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod };
}
- return {};
+ return { authMethod: decodedToken.authMethod, orgId: null };
};
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts
index 786e69d73..fa7439af8 100644
--- a/backend/src/services/auth/auth-login-service.ts
+++ b/backend/src/services/auth/auth-login-service.ts
@@ -1,13 +1,16 @@
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 { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
-import { BadRequestError } from "@app/lib/errors";
+import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
+import { TTokenDALFactory } from "../auth-token/auth-token-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
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";
@@ -17,16 +20,24 @@ import {
TOauthLoginDTO,
TVerifyMfaTokenDTO
} from "./auth-login-type";
-import { AuthMethod, AuthTokenType } from "./auth-type";
+import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
type TAuthLoginServiceFactoryDep = {
userDAL: TUserDALFactory;
+ orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
+ tokenDAL: TTokenDALFactory;
};
export type TAuthLoginFactory = ReturnType;
-export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: TAuthLoginServiceFactoryDep) => {
+export const authLoginServiceFactory = ({
+ userDAL,
+ tokenService,
+ smtpService,
+ orgDAL,
+ tokenDAL
+}: TAuthLoginServiceFactoryDep) => {
/*
* Private
* Not exported. This is to update user device list
@@ -83,12 +94,14 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
user,
ip,
userAgent,
- organizationId
+ organizationId,
+ authMethod
}: {
user: TUsers;
ip: string;
userAgent: string;
- organizationId?: string;
+ organizationId: string | undefined;
+ authMethod: AuthMethod;
}) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent);
@@ -98,8 +111,10 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
userId: user.id
});
if (!tokenSession) throw new Error("Failed to create token");
+
const accessToken = jwt.sign(
{
+ authMethod,
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: user.id,
tokenVersionId: tokenSession.id,
@@ -112,6 +127,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
const refreshToken = jwt.sign(
{
+ authMethod,
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: user.id,
tokenVersionId: tokenSession.id,
@@ -137,7 +153,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
username: email
});
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
- throw new Error("Failed to find user");
+ throw new Error("Failed to find user");
}
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
validateProviderAuthToken(providerAuthToken as string, email);
@@ -158,9 +174,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
const loginExchangeClientProof = async ({
email,
clientProof,
- providerAuthToken,
ip,
- userAgent
+ userAgent,
+ providerAuthToken
}: TLoginClientProofDTO) => {
const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
@@ -168,14 +184,16 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
if (!userEnc) throw new Error("Failed to find user");
const cfg = getConfig();
- let organizationId;
- if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
- const { orgId } = validateProviderAuthToken(providerAuthToken as string, email);
- organizationId = orgId;
- } else if (providerAuthToken) {
- // SAML SSO
- const { orgId } = validateProviderAuthToken(providerAuthToken, email);
- organizationId = orgId;
+ let authMethod = AuthMethod.EMAIL;
+ let organizationId: string | undefined;
+
+ if (providerAuthToken) {
+ const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
+
+ authMethod = decodedProviderToken.authMethod;
+ if (isAuthMethodSaml(authMethod) && decodedProviderToken.orgId) {
+ organizationId = decodedProviderToken.orgId;
+ }
}
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
@@ -196,9 +214,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
if (userEnc.isMfaEnabled && userEnc.email) {
const mfaToken = jwt.sign(
{
+ authMethod,
authTokenType: AuthTokenType.MFA_TOKEN,
- userId: userEnc.userId,
- organizationId
+ userId: userEnc.userId
},
cfg.AUTH_SECRET,
{
@@ -221,12 +239,60 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
},
ip,
userAgent,
+ authMethod,
organizationId
});
return { token, isMfaEnabled: false, user: userEnc } as const;
};
+ const selectOrganization = async ({
+ userAgent,
+ authJwtToken,
+ ipAddress,
+ organizationId
+ }: {
+ userAgent: string | undefined;
+ authJwtToken: string | undefined;
+ ipAddress: string;
+ organizationId: string;
+ }) => {
+ const cfg = getConfig();
+
+ if (!authJwtToken) throw new UnauthorizedError({ name: "Authorization header is required" });
+ if (!userAgent) throw new UnauthorizedError({ name: "user agent header is required" });
+
+ // eslint-disable-next-line no-param-reassign
+ authJwtToken = authJwtToken.replace("Bearer ", ""); // remove bearer from token
+
+ // The decoded JWT token, which contains the auth method.
+ const decodedToken = jwt.verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
+ if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" });
+
+ const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
+ if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
+
+ // Check if the user actually has access to the specified organization.
+ const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
+ const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId);
+
+ if (!hasOrganizationMembership) {
+ throw new UnauthorizedError({ message: "User does not have access to the organization" });
+ }
+
+ await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId);
+
+ const tokens = await generateUserTokens({
+ authMethod: decodedToken.authMethod,
+ user,
+ userAgent,
+ ip: ipAddress,
+ organizationId
+ });
+
+ return tokens;
+ };
+
/*
* Multi factor authentication re-send code, Get user id from token
* saved in frontend
@@ -244,12 +310,15 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
* Multi factor authentication verification of code
* Third step of login in which user completes with mfa
* */
- const verifyMfaToken = async ({ userId, mfaToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
+ const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId,
code: mfaToken
});
+
+ 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");
@@ -260,7 +329,8 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
},
ip,
userAgent,
- organizationId: orgId
+ organizationId: orgId,
+ authMethod: decodedToken.authMethod
});
return { token, user: userEnc };
@@ -339,6 +409,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
oauth2Login,
resendMfaToken,
verifyMfaToken,
+ selectOrganization,
generateUserTokens
};
};
diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts
index 86af5a5f9..37b90f548 100644
--- a/backend/src/services/auth/auth-login-type.ts
+++ b/backend/src/services/auth/auth-login-type.ts
@@ -17,6 +17,7 @@ export type TLoginClientProofDTO = {
export type TVerifyMfaTokenDTO = {
userId: string;
mfaToken: string;
+ mfaJwtToken: string;
ip: string;
userAgent: string;
orgId?: string;
diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts
index 42b8c2c39..4025e4903 100644
--- a/backend/src/services/auth/auth-password-service.ts
+++ b/backend/src/services/auth/auth-password-service.ts
@@ -192,7 +192,7 @@ export const authPaswordServiceFactory = ({
}: TCreateBackupPrivateKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
- throw new Error("Failed to find user");
+ throw new Error("Failed to find user");
}
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
@@ -239,7 +239,7 @@ export const authPaswordServiceFactory = ({
const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) {
- throw new Error("Failed to find user");
+ throw new Error("Failed to find user");
}
const backupKey = await authDAL.getBackupPrivateKeyByUserId(userId);
if (!backupKey) throw new Error("Failed to find user backup key");
diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts
index 39bfec8b1..3db935769 100644
--- a/backend/src/services/auth/auth-signup-service.ts
+++ b/backend/src/services/auth/auth-signup-service.ts
@@ -150,11 +150,15 @@ export const authSignupServiceFactory = ({
});
if (!organizationId) {
- await orgService.createOrganization({
+ const newOrganization = await orgService.createOrganization({
userId: user.id,
userEmail: user.email ?? user.username,
orgName: organizationName
});
+
+ if (!newOrganization) throw new Error("Failed to create organization");
+
+ organizationId = newOrganization.id;
}
const updatedMembersips = await orgDAL.updateMembership(
@@ -174,6 +178,7 @@ export const authSignupServiceFactory = ({
const accessToken = jwt.sign(
{
+ authMethod: AuthMethod.EMAIL,
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
@@ -186,6 +191,7 @@ export const authSignupServiceFactory = ({
const refreshToken = jwt.sign(
{
+ authMethod: AuthMethod.EMAIL,
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
@@ -196,7 +202,7 @@ export const authSignupServiceFactory = ({
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
);
- return { user: updateduser.info, accessToken, refreshToken };
+ return { user: updateduser.info, accessToken, refreshToken, organizationId };
};
/*
@@ -277,6 +283,7 @@ export const authSignupServiceFactory = ({
const accessToken = jwt.sign(
{
+ authMethod: AuthMethod.EMAIL,
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
@@ -288,6 +295,7 @@ export const authSignupServiceFactory = ({
const refreshToken = jwt.sign(
{
+ authMethod: AuthMethod.EMAIL,
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: updateduser.info.id,
tokenVersionId: tokenSession.id,
diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts
index 57c86158f..8e7b92253 100644
--- a/backend/src/services/auth/auth-type.ts
+++ b/backend/src/services/auth/auth-type.ts
@@ -6,6 +6,8 @@ export enum AuthMethod {
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
+ GOOGLE_SAML = "google-saml",
+ KEYCLOAK_SAML = "keycloak-saml",
LDAP = "ldap"
}
@@ -38,8 +40,12 @@ export enum ActorType { // would extend to AWS, Azure, ...
SCIM_CLIENT = "scimClient"
}
+// This will be null unless the token-type is JWT
+export type ActorAuthMethod = AuthMethod | null;
+
export type AuthModeJwtTokenPayload = {
authTokenType: AuthTokenType.ACCESS_TOKEN;
+ authMethod: AuthMethod;
userId: string;
tokenVersionId: string;
accessVersion: number;
@@ -48,12 +54,15 @@ export type AuthModeJwtTokenPayload = {
export type AuthModeMfaJwtTokenPayload = {
authTokenType: AuthTokenType.MFA_TOKEN;
+ authMethod: AuthMethod;
userId: string;
organizationId?: string;
};
export type AuthModeRefreshJwtTokenPayload = {
+ // authMode
authTokenType: AuthTokenType.REFRESH_TOKEN;
+ authMethod: AuthMethod;
userId: string;
tokenVersionId: string;
refreshVersion: number;
@@ -63,6 +72,8 @@ export type AuthModeRefreshJwtTokenPayload = {
export type AuthModeProviderJwtTokenPayload = {
authTokenType: AuthTokenType.PROVIDER_TOKEN;
username: string;
+ authMethod: AuthMethod;
+ email: string;
organizationId?: string;
};
diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts
new file mode 100644
index 000000000..3b0523dde
--- /dev/null
+++ b/backend/src/services/group-project/group-project-dal.ts
@@ -0,0 +1,99 @@
+import { Knex } from "knex";
+
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
+import { ormify, sqlNestRelationships } from "@app/lib/knex";
+
+export type TGroupProjectDALFactory = ReturnType;
+
+export const groupProjectDALFactory = (db: TDbClient) => {
+ const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
+
+ const findByProjectId = async (projectId: string, tx?: Knex) => {
+ try {
+ const docs = await (tx || db)(TableName.GroupProjectMembership)
+ .where(`${TableName.GroupProjectMembership}.projectId`, projectId)
+ .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`)
+ .join(
+ TableName.GroupProjectMembershipRole,
+ `${TableName.GroupProjectMembershipRole}.projectMembershipId`,
+ `${TableName.GroupProjectMembership}.id`
+ )
+ .leftJoin(
+ TableName.ProjectRoles,
+ `${TableName.GroupProjectMembershipRole}.customRoleId`,
+ `${TableName.ProjectRoles}.id`
+ )
+ .select(
+ db.ref("id").withSchema(TableName.GroupProjectMembership),
+ db.ref("createdAt").withSchema(TableName.GroupProjectMembership),
+ db.ref("updatedAt").withSchema(TableName.GroupProjectMembership),
+ db.ref("id").as("groupId").withSchema(TableName.Groups),
+ db.ref("name").as("groupName").withSchema(TableName.Groups),
+ db.ref("slug").as("groupSlug").withSchema(TableName.Groups),
+ db.ref("id").withSchema(TableName.GroupProjectMembership),
+ db.ref("role").withSchema(TableName.GroupProjectMembershipRole),
+ db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"),
+ db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole),
+ db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
+ db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
+ db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole),
+ db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole),
+ db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
+ db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
+ db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole)
+ );
+
+ const members = sqlNestRelationships({
+ data: docs,
+ parentMapper: ({ groupId, groupName, groupSlug, id, createdAt, updatedAt }) => ({
+ id,
+ groupId,
+ createdAt,
+ updatedAt,
+ group: {
+ id: groupId,
+ name: groupName,
+ slug: groupSlug
+ }
+ }),
+ key: "id",
+ childrenMapper: [
+ {
+ label: "roles" as const,
+ key: "membershipRoleId",
+ mapper: ({
+ role,
+ customRoleId,
+ customRoleName,
+ customRoleSlug,
+ membershipRoleId,
+ temporaryRange,
+ temporaryMode,
+ temporaryAccessEndTime,
+ temporaryAccessStartTime,
+ isTemporary
+ }) => ({
+ id: membershipRoleId,
+ role,
+ customRoleId,
+ customRoleName,
+ customRoleSlug,
+ temporaryRange,
+ temporaryMode,
+ temporaryAccessEndTime,
+ temporaryAccessStartTime,
+ isTemporary
+ })
+ }
+ ]
+ });
+ return members;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "FindByProjectId" });
+ }
+ };
+
+ return { ...groupProjectOrm, findByProjectId };
+};
diff --git a/backend/src/services/group-project/group-project-membership-role-dal.ts b/backend/src/services/group-project/group-project-membership-role-dal.ts
new file mode 100644
index 000000000..5572ac6f5
--- /dev/null
+++ b/backend/src/services/group-project/group-project-membership-role-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 TGroupProjectMembershipRoleDALFactory = ReturnType;
+
+export const groupProjectMembershipRoleDALFactory = (db: TDbClient) => {
+ const orm = ormify(db, TableName.GroupProjectMembershipRole);
+ return orm;
+};
diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts
new file mode 100644
index 000000000..589d0e474
--- /dev/null
+++ b/backend/src/services/group-project/group-project-service.ts
@@ -0,0 +1,338 @@
+import { ForbiddenError } from "@casl/ability";
+import ms from "ms";
+
+import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
+import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import { isAtLeastAsPrivileged } from "@app/lib/casl";
+import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
+import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
+import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
+import { groupBy } from "@app/lib/fn";
+
+import { TGroupDALFactory } from "../../ee/services/group/group-dal";
+import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
+import { TProjectDALFactory } from "../project/project-dal";
+import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
+import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
+import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
+import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
+import { TGroupProjectDALFactory } from "./group-project-dal";
+import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membership-role-dal";
+import {
+ TCreateProjectGroupDTO,
+ TDeleteProjectGroupDTO,
+ TListProjectGroupDTO,
+ TUpdateProjectGroupDTO
+} from "./group-project-types";
+
+type TGroupProjectServiceFactoryDep = {
+ groupProjectDAL: Pick;
+ groupProjectMembershipRoleDAL: Pick<
+ TGroupProjectMembershipRoleDALFactory,
+ "create" | "transaction" | "insertMany" | "delete"
+ >;
+ userGroupMembershipDAL: TUserGroupMembershipDALFactory;
+ projectDAL: Pick;
+ projectKeyDAL: Pick;
+ projectRoleDAL: Pick;
+ projectBotDAL: TProjectBotDALFactory;
+ groupDAL: Pick;
+ permissionService: Pick;
+};
+
+export type TGroupProjectServiceFactory = ReturnType;
+
+export const groupProjectServiceFactory = ({
+ groupDAL,
+ groupProjectDAL,
+ groupProjectMembershipRoleDAL,
+ userGroupMembershipDAL,
+ projectDAL,
+ projectKeyDAL,
+ projectBotDAL,
+ projectRoleDAL,
+ permissionService
+}: TGroupProjectServiceFactoryDep) => {
+ const addGroupToProject = async ({
+ groupSlug,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ projectSlug,
+ role
+ }: TCreateProjectGroupDTO) => {
+ const project = await projectDAL.findOne({
+ slug: projectSlug
+ });
+
+ if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
+ if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
+
+ const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
+ if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
+
+ const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
+ if (existingGroup)
+ throw new BadRequestError({
+ message: `Group with slug ${groupSlug} already exists in project with id ${project.id}`
+ });
+
+ const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole(
+ role,
+ project.id
+ );
+ const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
+ if (!hasPrivilege)
+ throw new ForbiddenRequestError({
+ message: "Failed to add group to project with more privileged role"
+ });
+ const isCustomRole = Boolean(customRole);
+
+ const projectGroup = await groupProjectDAL.transaction(async (tx) => {
+ const groupProjectMembership = await groupProjectDAL.create(
+ {
+ groupId: group.id,
+ projectId: project.id
+ },
+ tx
+ );
+
+ await groupProjectMembershipRoleDAL.create(
+ {
+ projectMembershipId: groupProjectMembership.id,
+ role: isCustomRole ? ProjectMembershipRole.Custom : role,
+ customRoleId: customRole?.id
+ },
+ tx
+ );
+ return groupProjectMembership;
+ });
+
+ // share project key with users in group that have not
+ // individually been added to the project and that are not part of
+ // other groups that are in the project
+ const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
+
+ if (groupMembers.length) {
+ const ghostUser = await projectDAL.findProjectGhostUser(project.id);
+
+ if (!ghostUser) {
+ throw new BadRequestError({
+ message: "Failed to find sudo user"
+ });
+ }
+
+ const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id);
+
+ if (!ghostUserLatestKey) {
+ throw new BadRequestError({
+ message: "Failed to find sudo user latest key"
+ });
+ }
+
+ const bot = await projectBotDAL.findOne({ projectId: project.id });
+
+ if (!bot) {
+ throw new BadRequestError({
+ message: "Failed to find bot"
+ });
+ }
+
+ const botPrivateKey = infisicalSymmetricDecrypt({
+ keyEncoding: bot.keyEncoding as SecretKeyEncoding,
+ iv: bot.iv,
+ tag: bot.tag,
+ ciphertext: bot.encryptedPrivateKey
+ });
+
+ const plaintextProjectKey = decryptAsymmetric({
+ ciphertext: ghostUserLatestKey.encryptedKey,
+ nonce: ghostUserLatestKey.nonce,
+ publicKey: ghostUserLatestKey.sender.publicKey,
+ privateKey: botPrivateKey
+ });
+
+ const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
+ const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, publicKey, botPrivateKey);
+
+ return {
+ encryptedKey,
+ nonce,
+ senderId: ghostUser.id,
+ receiverId: id,
+ projectId: project.id
+ };
+ });
+
+ await projectKeyDAL.insertMany(projectKeyData);
+ }
+
+ return projectGroup;
+ };
+
+ const updateGroupInProject = async ({
+ projectSlug,
+ groupSlug,
+ roles,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TUpdateProjectGroupDTO) => {
+ const project = await projectDAL.findOne({
+ slug: projectSlug
+ });
+
+ if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
+
+ const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
+ if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
+
+ const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
+ if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
+
+ // validate custom roles input
+ const customInputRoles = roles.filter(
+ ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
+ );
+ const hasCustomRole = Boolean(customInputRoles.length);
+ const customRoles = hasCustomRole
+ ? await projectRoleDAL.find({
+ projectId: project.id,
+ $in: { slug: customInputRoles.map(({ role }) => role) }
+ })
+ : [];
+ if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
+
+ const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
+
+ const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
+ const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
+ if (!inputRole.isTemporary) {
+ return {
+ projectMembershipId: projectGroup.id,
+ role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
+ customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
+ };
+ }
+
+ // check cron or relative here later for now its just relative
+ const relativeTimeInMs = ms(inputRole.temporaryRange);
+ return {
+ projectMembershipId: projectGroup.id,
+ role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
+ customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
+ isTemporary: true,
+ temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
+ temporaryRange: inputRole.temporaryRange,
+ temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
+ temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
+ };
+ });
+
+ const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => {
+ await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx);
+ return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
+ });
+
+ return updatedRoles;
+ };
+
+ const removeGroupFromProject = async ({
+ projectSlug,
+ groupSlug,
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod
+ }: TDeleteProjectGroupDTO) => {
+ const project = await projectDAL.findOne({
+ slug: projectSlug
+ });
+
+ if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
+
+ const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug });
+ if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
+
+ const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
+ if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
+
+ const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id);
+
+ if (groupMembers.length) {
+ await projectKeyDAL.delete({
+ projectId: project.id,
+ $in: {
+ receiverId: groupMembers.map(({ user: { id } }) => id)
+ }
+ });
+ }
+
+ const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id });
+
+ return deletedGroup;
+ };
+
+ const listGroupsInProject = async ({
+ projectSlug,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TListProjectGroupDTO) => {
+ const project = await projectDAL.findOne({
+ slug: projectSlug
+ });
+
+ if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
+
+ const groupMemberships = await groupProjectDAL.findByProjectId(project.id);
+ return groupMemberships;
+ };
+
+ return {
+ addGroupToProject,
+ updateGroupInProject,
+ removeGroupFromProject,
+ listGroupsInProject
+ };
+};
diff --git a/backend/src/services/group-project/group-project-types.ts b/backend/src/services/group-project/group-project-types.ts
new file mode 100644
index 000000000..c867b75c0
--- /dev/null
+++ b/backend/src/services/group-project/group-project-types.ts
@@ -0,0 +1,31 @@
+import { TProjectSlugPermission } from "@app/lib/types";
+
+import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
+
+export type TCreateProjectGroupDTO = {
+ groupSlug: string;
+ role: string;
+} & TProjectSlugPermission;
+
+export type TUpdateProjectGroupDTO = {
+ roles: (
+ | {
+ role: string;
+ isTemporary?: false;
+ }
+ | {
+ role: string;
+ isTemporary: true;
+ temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ }
+ )[];
+ groupSlug: string;
+} & TProjectSlugPermission;
+
+export type TDeleteProjectGroupDTO = {
+ groupSlug: string;
+} & TProjectSlugPermission;
+
+export type TListProjectGroupDTO = TProjectSlugPermission;
diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts
index 32774ccbb..4b53c8174 100644
--- a/backend/src/services/identity-access-token/identity-access-token-service.ts
+++ b/backend/src/services/identity-access-token/identity-access-token-service.ts
@@ -6,17 +6,20 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
import { AuthTokenType } from "../auth/auth-type";
+import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types";
type TIdentityAccessTokenServiceFactoryDep = {
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
+ identityOrgMembershipDAL: TIdentityOrgDALFactory;
};
export type TIdentityAccessTokenServiceFactory = ReturnType;
export const identityAccessTokenServiceFactory = ({
- identityAccessTokenDAL
+ identityAccessTokenDAL,
+ identityOrgMembershipDAL
}: TIdentityAccessTokenServiceFactoryDep) => {
const validateAccessTokenExp = (identityAccessToken: TIdentityAccessTokens) => {
const {
@@ -117,8 +120,16 @@ export const identityAccessTokenServiceFactory = ({
});
}
+ const identityOrgMembership = await identityOrgMembershipDAL.findOne({
+ identityId: identityAccessToken.identityId
+ });
+
+ if (!identityOrgMembership) {
+ throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
+ }
+
validateAccessTokenExp(identityAccessToken);
- return identityAccessToken;
+ return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
};
return { renewAccessToken, fnValidateIdentityAccessToken };
diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts
index dd3ba04f4..e932d2068 100644
--- a/backend/src/services/identity-project/identity-project-dal.ts
+++ b/backend/src/services/identity-project/identity-project-dal.ts
@@ -25,6 +25,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
+ .leftJoin(
+ TableName.IdentityProjectAdditionalPrivilege,
+ `${TableName.IdentityProjectMembership}.id`,
+ `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
+ )
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts
index b6f6e4343..18a1803ac 100644
--- a/backend/src/services/identity-project/identity-project-service.ts
+++ b/backend/src/services/identity-project/identity-project-service.ts
@@ -49,10 +49,17 @@ export const identityProjectServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
projectId,
role
}: TCreateProjectIdentityDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId });
@@ -86,9 +93,7 @@ export const identityProjectServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.create(
{
identityId,
- projectId: project.id,
- role: isCustomRole ? ProjectMembershipRole.Custom : role,
- roleId: customRole?.id
+ projectId: project.id
},
tx
);
@@ -112,9 +117,16 @@ export const identityProjectServiceFactory = ({
roles,
actor,
actorId,
+ actorAuthMethod,
actorOrgId
}: TUpdateProjectIdentityDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId });
@@ -127,6 +139,7 @@ export const identityProjectServiceFactory = ({
ActorType.IDENTITY,
projectIdentity.identityId,
projectIdentity.projectId,
+ actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
@@ -148,7 +161,7 @@ export const identityProjectServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
- const santiziedProjectMembershipRoles = roles.map((inputRole) => {
+ const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
@@ -174,7 +187,7 @@ export const identityProjectServiceFactory = ({
const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => {
await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx);
- return identityProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
+ return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
return updatedRoles;
@@ -185,6 +198,7 @@ export const identityProjectServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
projectId
}: TDeleteProjectIdentityDTO) => {
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
@@ -195,6 +209,7 @@ export const identityProjectServiceFactory = ({
actor,
actorId,
identityProjectMembership.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
@@ -202,6 +217,7 @@ export const identityProjectServiceFactory = ({
ActorType.IDENTITY,
identityId,
identityProjectMembership.projectId,
+ actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
@@ -212,12 +228,24 @@ export const identityProjectServiceFactory = ({
return deletedIdentity;
};
- const listProjectIdentities = async ({ projectId, actor, actorId, actorOrgId }: TListProjectIdentityDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const listProjectIdentities = async ({
+ projectId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TListProjectIdentityDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
- const identityMemberhips = await identityProjectDAL.findByProjectId(projectId);
- return identityMemberhips;
+ const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
+ return identityMemberships;
};
return {
diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts
index d375a8fa5..54a074073 100644
--- a/backend/src/services/identity-ua/identity-ua-service.ts
+++ b/backend/src/services/identity-ua/identity-ua-service.ts
@@ -144,6 +144,7 @@ export const identityUaServiceFactory = ({
accessTokenTrustedIps,
clientSecretTrustedIps,
actorId,
+ actorAuthMethod,
actor,
actorOrgId
}: TAttachUaDTO) => {
@@ -162,6 +163,7 @@ export const identityUaServiceFactory = ({
actor,
actorId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
@@ -233,6 +235,7 @@ export const identityUaServiceFactory = ({
accessTokenTrustedIps,
clientSecretTrustedIps,
actorId,
+ actorAuthMethod,
actor,
actorOrgId
}: TUpdateUaDTO) => {
@@ -256,6 +259,7 @@ export const identityUaServiceFactory = ({
actor,
actorId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
@@ -308,7 +312,7 @@ export const identityUaServiceFactory = ({
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
};
- const getIdentityUa = async ({ identityId, actorId, actor, actorOrgId }: TGetUaDTO) => {
+ const getIdentityUa = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
@@ -322,6 +326,7 @@ export const identityUaServiceFactory = ({
actor,
actorId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
@@ -334,6 +339,7 @@ export const identityUaServiceFactory = ({
actorOrgId,
identityId,
ttl,
+ actorAuthMethod,
description,
numUsesLimit
}: TCreateUaClientSecretDTO) => {
@@ -347,6 +353,7 @@ export const identityUaServiceFactory = ({
actor,
actorId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
@@ -355,6 +362,7 @@ export const identityUaServiceFactory = ({
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
@@ -388,7 +396,13 @@ export const identityUaServiceFactory = ({
};
};
- const getUaClientSecrets = async ({ actor, actorId, actorOrgId, identityId }: TGetUaClientSecretsDTO) => {
+ const getUaClientSecrets = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ identityId
+ }: TGetUaClientSecretsDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
@@ -399,6 +413,7 @@ export const identityUaServiceFactory = ({
actor,
actorId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
@@ -407,6 +422,7 @@ export const identityUaServiceFactory = ({
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
@@ -431,6 +447,7 @@ export const identityUaServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
clientSecretId
}: TRevokeUaClientSecretDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
@@ -443,6 +460,7 @@ export const identityUaServiceFactory = ({
actor,
actorId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
@@ -451,6 +469,7 @@ export const identityUaServiceFactory = ({
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
+ actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts
index e37a3a6dd..2863bf23e 100644
--- a/backend/src/services/identity/identity-service.ts
+++ b/backend/src/services/identity/identity-service.ts
@@ -25,8 +25,16 @@ export const identityServiceFactory = ({
identityOrgMembershipDAL,
permissionService
}: TIdentityServiceFactoryDep) => {
- const createIdentity = async ({ name, role, actor, orgId, actorId, actorOrgId }: TCreateIdentityDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const createIdentity = async ({
+ name,
+ role,
+ actor,
+ orgId,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TCreateIdentityDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
@@ -54,7 +62,15 @@ export const identityServiceFactory = ({
return identity;
};
- const updateIdentity = async ({ id, role, name, actor, actorId, actorOrgId }: TUpdateIdentityDTO) => {
+ const updateIdentity = async ({
+ id,
+ role,
+ name,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TUpdateIdentityDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
@@ -62,6 +78,7 @@ export const identityServiceFactory = ({
actor,
actorId,
identityOrgMembership.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
@@ -70,6 +87,7 @@ export const identityServiceFactory = ({
ActorType.IDENTITY,
id,
identityOrgMembership.orgId,
+ actorAuthMethod,
actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
@@ -108,7 +126,7 @@ export const identityServiceFactory = ({
return { ...identity, orgId: identityOrgMembership.orgId };
};
- const deleteIdentity = async ({ actorId, actor, actorOrgId, id }: TDeleteIdentityDTO) => {
+ const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
@@ -116,13 +134,16 @@ export const identityServiceFactory = ({
actor,
actorId,
identityOrgMembership.orgId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
id,
- identityOrgMembership.orgId
+ identityOrgMembership.orgId,
+ actorAuthMethod,
+ actorOrgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
@@ -132,12 +153,12 @@ export const identityServiceFactory = ({
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
};
- const listOrgIdentities = async ({ orgId, actor, actorId, actorOrgId }: TOrgPermission) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const listOrgIdentities = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPermission) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
- const identityMemberhips = await identityOrgMembershipDAL.findByOrgId(orgId);
- return identityMemberhips;
+ const identityMemberships = await identityOrgMembershipDAL.findByOrgId(orgId);
+ return identityMemberships;
};
return {
diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts
index 62de06eb0..9cb0d822c 100644
--- a/backend/src/services/integration-auth/integration-app-list.ts
+++ b/backend/src/services/integration-auth/integration-app-list.ts
@@ -129,26 +129,55 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* Return list of names of apps for Vercel integration
*/
const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
- const res = (
- await request.get<{ projects: { name: string; id: string }[] }>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
+ const apps: Array<{ name: string; appId: string }> = [];
+
+ const limit = "20";
+ let hasMorePages = true;
+ let next: number | null = null;
+
+ interface Response {
+ projects: { name: string; id: string }[];
+ pagination: {
+ count: number;
+ next: number | null;
+ prev: number;
+ };
+ }
+
+ while (hasMorePages) {
+ const params: { [key: string]: string } = {
+ limit
+ };
+
+ if (teamId) {
+ params.teamId = teamId;
+ }
+
+ if (next) {
+ params.until = String(next);
+ }
+
+ const { data } = await request.get(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
+ params: new URLSearchParams(params),
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
- },
- ...(teamId
- ? {
- params: {
- teamId
- }
- }
- : {})
- })
- ).data;
+ }
+ });
- const apps = res.projects.map((a) => ({
- name: a.name,
- appId: a.id
- }));
+ data.projects.forEach((a) => {
+ apps.push({
+ name: a.name,
+ appId: a.id
+ });
+ });
+
+ next = data.pagination.next;
+
+ if (data.pagination.next === null) {
+ hasMorePages = false;
+ }
+ }
return apps;
};
@@ -260,20 +289,44 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
* Return list of services for Render integration
*/
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
- const res = (
- await request.get<{ service: { name: string; id: string } }[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
- headers: {
- Authorization: `Bearer ${accessToken}`,
- Accept: "application/json",
- "Accept-Encoding": "application/json"
- }
- })
- ).data;
+ const apps: Array<{ name: string; appId: string }> = [];
+ let hasMorePages = true;
+ const perPage = 100;
+ let cursor;
- const apps = res.map((a) => ({
- name: a.service.name,
- appId: a.service.id
- }));
+ interface RenderService {
+ cursor: string;
+ service: { name: string; id: string };
+ }
+
+ while (hasMorePages) {
+ const res: RenderService[] = (
+ await request.get(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
+ params: new URLSearchParams({
+ ...(cursor ? { cursor: String(cursor) } : {}),
+ limit: String(perPage)
+ }),
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ Accept: "application/json",
+ "Accept-Encoding": "application/json"
+ }
+ })
+ ).data;
+
+ res.forEach((a) => {
+ apps.push({
+ name: a.service.name,
+ appId: a.service.id
+ });
+ });
+
+ if (res.length < perPage) {
+ hasMorePages = false;
+ } else {
+ cursor = res[res.length - 1].cursor;
+ }
+ }
return apps;
};
diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts
index 4cd0bc159..3d42943a6 100644
--- a/backend/src/services/integration-auth/integration-auth-service.ts
+++ b/backend/src/services/integration-auth/integration-auth-service.ts
@@ -1,4 +1,6 @@
import { ForbiddenError } from "@casl/ability";
+import { Octokit } from "@octokit/rest";
+import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -22,8 +24,11 @@ import {
TGetIntegrationAuthTeamCityBuildConfigDTO,
THerokuPipelineCoupling,
TIntegrationAuthAppsDTO,
+ TIntegrationAuthAwsKmsKeyDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
+ TIntegrationAuthGithubEnvsDTO,
+ TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO,
TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthQoveryEnvironmentsDTO,
@@ -61,14 +66,26 @@ export const integrationAuthServiceFactory = ({
projectBotDAL,
projectBotService
}: TIntegrationAuthServiceFactoryDep) => {
- const listIntegrationAuthByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TProjectPermission) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const listIntegrationAuthByProjectId = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TProjectPermission) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const authorizations = await integrationAuthDAL.find({ projectId });
return authorizations;
};
- const getIntegrationAuth = async ({ actor, id, actorId, actorOrgId }: TGetIntegrationAuthDTO) => {
+ const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -76,6 +93,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -87,6 +105,7 @@ export const integrationAuthServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
integration,
url,
code
@@ -94,7 +113,13 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const bot = await projectBotDAL.findOne({ isActive: true, projectId });
@@ -150,6 +175,7 @@ export const integrationAuthServiceFactory = ({
url,
actor,
actorOrgId,
+ actorAuthMethod,
accessId,
namespace,
accessToken
@@ -157,7 +183,13 @@ export const integrationAuthServiceFactory = ({
if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const bot = await projectBotDAL.findOne({ isActive: true, projectId });
@@ -274,6 +306,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
teamId,
id,
workspaceSlug
@@ -285,6 +318,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -302,7 +336,13 @@ export const integrationAuthServiceFactory = ({
return apps;
};
- const getIntegrationAuthTeams = async ({ actor, actorId, actorOrgId, id }: TIntegrationAuthTeamsDTO) => {
+ const getIntegrationAuthTeams = async ({
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId,
+ id
+ }: TIntegrationAuthTeamsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -310,6 +350,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -324,7 +365,14 @@ export const integrationAuthServiceFactory = ({
return teams;
};
- const getVercelBranches = async ({ appId, id, actor, actorId, actorOrgId }: TIntegrationAuthVercelBranchesDTO) => {
+ const getVercelBranches = async ({
+ appId,
+ id,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TIntegrationAuthVercelBranchesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -332,6 +380,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -357,7 +406,14 @@ export const integrationAuthServiceFactory = ({
return [];
};
- const getChecklyGroups = async ({ actorId, actor, actorOrgId, id, accountId }: TIntegrationAuthChecklyGroupsDTO) => {
+ const getChecklyGroups = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ id,
+ accountId
+ }: TIntegrationAuthChecklyGroupsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -365,6 +421,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -383,7 +440,7 @@ export const integrationAuthServiceFactory = ({
return [];
};
- const getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => {
+ const getGithubOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthGithubOrgsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -391,6 +448,76 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
+ const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
+ const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
+
+ const octokit = new Octokit({
+ auth: accessToken
+ });
+
+ const { data } = await octokit.request("GET /user/orgs", {
+ headers: {
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+ });
+ if (!data) return [];
+
+ return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) }));
+ };
+
+ const getGithubEnvs = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ id,
+ repoOwner,
+ repoName
+ }: TIntegrationAuthGithubEnvsDTO) => {
+ const integrationAuth = await integrationAuthDAL.findById(id);
+ if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ integrationAuth.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
+ const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
+ const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
+
+ const octokit = new Octokit({
+ auth: accessToken
+ });
+
+ const {
+ data: { environments }
+ } = await octokit.request("GET /repos/{owner}/{repo}/environments", {
+ headers: {
+ "X-GitHub-Api-Version": "2022-11-28"
+ },
+ owner: repoOwner,
+ repo: repoName
+ });
+ if (!environments) return [];
+ return environments.map(({ id: envId, name }) => ({ name, envId: String(envId) }));
+ };
+
+ const getQoveryOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthQoveryOrgsDTO) => {
+ const integrationAuth = await integrationAuthDAL.findById(id);
+ if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -409,7 +536,14 @@ export const integrationAuthServiceFactory = ({
return data.results.map(({ name, id: orgId }) => ({ name, orgId }));
};
- const getQoveryProjects = async ({ actorId, actor, actorOrgId, id, orgId }: TIntegrationAuthQoveryProjectDTO) => {
+ const getAwsKmsKeys = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ id,
+ region
+ }: TIntegrationAuthAwsKmsKeyDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -417,6 +551,53 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
+ const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
+ const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
+
+ AWS.config.update({
+ region,
+ credentials: {
+ accessKeyId: String(accessId),
+ secretAccessKey: accessToken
+ }
+ });
+ const kms = new AWS.KMS();
+
+ const aliases = await kms.listAliases({}).promise();
+ const keys = await kms.listKeys({}).promise();
+ const response = keys
+ .Keys!.map((key) => {
+ const keyAlias = aliases.Aliases!.find((alias) => key.KeyId === alias.TargetKeyId);
+ if (!keyAlias?.AliasName?.includes("alias/aws/") || keyAlias?.AliasName?.includes("alias/aws/secretsmanager")) {
+ return { id: String(key.KeyId), alias: String(keyAlias?.AliasName || key.KeyId) };
+ }
+ return { id: "null", alias: "null" };
+ })
+ .filter((elem) => elem.id !== "null");
+
+ return response;
+ };
+
+ const getQoveryProjects = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ id,
+ orgId
+ }: TIntegrationAuthQoveryProjectDTO) => {
+ const integrationAuth = await integrationAuthDAL.findById(id);
+ if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -442,6 +623,7 @@ export const integrationAuthServiceFactory = ({
id,
actor,
actorId,
+ actorAuthMethod,
actorOrgId
}: TIntegrationAuthQoveryEnvironmentsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
@@ -451,6 +633,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -476,7 +659,14 @@ export const integrationAuthServiceFactory = ({
return [];
};
- const getQoveryApps = async ({ id, actor, actorId, actorOrgId, environmentId }: TIntegrationAuthQoveryScopesDTO) => {
+ const getQoveryApps = async ({
+ id,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ environmentId
+ }: TIntegrationAuthQoveryScopesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -484,6 +674,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -513,6 +704,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
environmentId
}: TIntegrationAuthQoveryScopesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
@@ -522,6 +714,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -546,7 +739,14 @@ export const integrationAuthServiceFactory = ({
return [];
};
- const getQoveryJobs = async ({ id, actor, actorId, actorOrgId, environmentId }: TIntegrationAuthQoveryScopesDTO) => {
+ const getQoveryJobs = async ({
+ id,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ environmentId
+ }: TIntegrationAuthQoveryScopesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -554,6 +754,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -578,7 +779,13 @@ export const integrationAuthServiceFactory = ({
return [];
};
- const getHerokuPipelines = async ({ id, actor, actorId, actorOrgId }: TIntegrationAuthHerokuPipelinesDTO) => {
+ const getHerokuPipelines = async ({
+ id,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TIntegrationAuthHerokuPipelinesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -586,6 +793,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -610,7 +818,14 @@ export const integrationAuthServiceFactory = ({
}));
};
- const getRailwayEnvironments = async ({ id, actor, actorId, actorOrgId, appId }: TIntegrationAuthRailwayEnvDTO) => {
+ const getRailwayEnvironments = async ({
+ id,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ appId
+ }: TIntegrationAuthRailwayEnvDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -618,6 +833,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -670,7 +886,14 @@ export const integrationAuthServiceFactory = ({
return [];
};
- const getRailwayServices = async ({ id, actor, actorId, actorOrgId, appId }: TIntegrationAuthRailwayServicesDTO) => {
+ const getRailwayServices = async ({
+ id,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ appId
+ }: TIntegrationAuthRailwayServicesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -678,6 +901,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -737,7 +961,13 @@ export const integrationAuthServiceFactory = ({
return [];
};
- const getBitbucketWorkspaces = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthBitbucketWorkspaceDTO) => {
+ const getBitbucketWorkspaces = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ id
+ }: TIntegrationAuthBitbucketWorkspaceDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -745,6 +975,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -756,9 +987,7 @@ export const integrationAuthServiceFactory = ({
while (hasNextPage) {
// eslint-disable-next-line
- const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(
- workspaceUrl,
- {
+ const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(workspaceUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
@@ -785,6 +1014,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
appId
}: TIntegrationAuthNorthflankSecretGroupDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
@@ -794,6 +1024,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -851,6 +1082,7 @@ export const integrationAuthServiceFactory = ({
id,
actorId,
actorOrgId,
+ actorAuthMethod,
actor
}: TGetIntegrationAuthTeamCityBuildConfigDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
@@ -860,6 +1092,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -891,16 +1124,29 @@ export const integrationAuthServiceFactory = ({
integration,
actor,
actorId,
+ actorAuthMethod,
actorOrgId
}: TDeleteIntegrationAuthsDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrations = await integrationAuthDAL.delete({ integration, projectId });
return integrations;
};
- const deleteIntegrationAuthById = async ({ id, actorId, actor, actorOrgId }: TDeleteIntegrationAuthByIdDTO) => {
+ const deleteIntegrationAuthById = async ({
+ id,
+ actorId,
+ actor,
+ actorAuthMethod,
+ actorOrgId
+ }: TDeleteIntegrationAuthByIdDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -908,6 +1154,7 @@ export const integrationAuthServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
@@ -934,6 +1181,9 @@ export const integrationAuthServiceFactory = ({
getIntegrationApps,
getVercelBranches,
getApps,
+ getAwsKmsKeys,
+ getGithubOrgs,
+ getGithubEnvs,
getChecklyGroups,
getQoveryApps,
getQoveryEnvs,
diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts
index e3dbc8341..0a816035c 100644
--- a/backend/src/services/integration-auth/integration-auth-types.ts
+++ b/backend/src/services/integration-auth/integration-auth-types.ts
@@ -44,6 +44,16 @@ export type TIntegrationAuthChecklyGroupsDTO = {
accountId: string;
} & Omit;
+export type TIntegrationAuthGithubOrgsDTO = {
+ id: string;
+} & Omit;
+
+export type TIntegrationAuthGithubEnvsDTO = {
+ id: string;
+ repoName: string;
+ repoOwner: string;
+} & Omit;
+
export type TIntegrationAuthQoveryOrgsDTO = {
id: string;
} & Omit;
@@ -53,6 +63,11 @@ export type TIntegrationAuthQoveryProjectDTO = {
orgId: string;
} & Omit;
+export type TIntegrationAuthAwsKmsKeyDTO = {
+ id: string;
+ region: string;
+} & Omit;
+
export type TIntegrationAuthQoveryEnvironmentsDTO = {
id: string;
} & TProjectPermission;
diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts
index 4083e3af2..bc880c8c4 100644
--- a/backend/src/services/integration-auth/integration-sync-secret.ts
+++ b/backend/src/services/integration-auth/integration-sync-secret.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
@@ -457,9 +458,11 @@ const syncSecretsAWSParameterStore = async ({
});
ssm.config.update(config);
+ const metadata = z.record(z.any()).parse(integration.metadata);
+
const params = {
Path: integration.path as string,
- Recursive: true,
+ Recursive: false,
WithDecryption: true
};
@@ -486,7 +489,10 @@ const syncSecretsAWSParameterStore = async ({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
- Overwrite: true
+ // Overwrite: true,
+ Tags: metadata.secretAWSTag
+ ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
+ : []
})
.promise();
// case: secret exists in AWS parameter store
@@ -499,6 +505,7 @@ const syncSecretsAWSParameterStore = async ({
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true
+ // Tags: metadata.secretAWSTag ? [{ Key: metadata.secretAWSTag.key, Value: metadata.secretAWSTag.value }] : []
})
.promise();
}
@@ -537,6 +544,7 @@ const syncSecretsAWSSecretManager = async ({
}) => {
let secretsManager;
const secKeyVal = getSecretKeyValuePair(secrets);
+ const metadata = z.record(z.any()).parse(integration.metadata);
try {
if (!accessId) return;
@@ -573,7 +581,11 @@ const syncSecretsAWSSecretManager = async ({
await secretsManager.send(
new CreateSecretCommand({
Name: integration.app as string,
- SecretString: JSON.stringify(secKeyVal)
+ SecretString: JSON.stringify(secKeyVal),
+ KmsKeyId: metadata.kmsKeyId ? metadata.kmsKeyId : null,
+ Tags: metadata.secretAWSTag
+ ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
+ : []
})
);
}
@@ -1110,98 +1122,176 @@ const syncSecretsGitHub = async ({
interface GitHubRepoKey {
key_id: string;
key: string;
+ id?: number | undefined;
+ url?: string | undefined;
+ title?: string | undefined;
+ created_at?: string | undefined;
}
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
- }
-
- interface GitHubSecretRes {
- [index: string]: GitHubSecret;
+ visibility?: "all" | "private" | "selected";
+ selected_repositories_url?: string | undefined;
}
const octokit = new Octokit({
auth: accessToken
});
- // const user = (await octokit.request('GET /user', {})).data;
- const repoPublicKey: GitHubRepoKey = (
- await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
- owner: integration.owner as string,
- repo: integration.app as string
- })
- ).data;
+ enum GithubScope {
+ Repo = "github-repo",
+ Org = "github-org",
+ Env = "github-env"
+ }
+
+ let repoPublicKey: GitHubRepoKey;
+
+ switch (integration.scope) {
+ case GithubScope.Org: {
+ const { data } = await octokit.request("GET /orgs/{org}/actions/secrets/public-key", {
+ org: integration.owner as string
+ });
+ repoPublicKey = data;
+ break;
+ }
+ case GithubScope.Env: {
+ const { data } = await octokit.request(
+ "GET /repositories/{repository_id}/environments/{environment_name}/secrets/public-key",
+ {
+ repository_id: Number(integration.appId),
+ environment_name: integration.targetEnvironmentId as string
+ }
+ );
+ repoPublicKey = data;
+ break;
+ }
+ default: {
+ const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
+ owner: integration.owner as string,
+ repo: integration.app as string
+ });
+ repoPublicKey = data;
+ break;
+ }
+ }
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
- let encryptedSecrets: GitHubSecretRes = (
- await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
- owner: integration.owner as string,
- repo: integration.app as string
- })
- ).data.secrets.reduce(
- (obj, secret) => ({
- ...obj,
- [secret.name]: secret
- }),
- {}
- );
+ let encryptedSecrets: GitHubSecret[];
- encryptedSecrets = Object.keys(encryptedSecrets).reduce(
- (
- result: {
- [key: string]: GitHubSecret;
- },
- key
- ) => {
- if (
- (appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
- (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
- ) {
- result[key] = encryptedSecrets[key];
- }
- return result;
- },
- {}
- );
-
- await Promise.all(
- Object.keys(encryptedSecrets).map(async (key) => {
- if (!(key in secrets)) {
- return octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
+ switch (integration.scope) {
+ case GithubScope.Org: {
+ encryptedSecrets = (
+ await octokit.request("GET /orgs/{org}/actions/secrets", {
+ org: integration.owner as string
+ })
+ ).data.secrets;
+ break;
+ }
+ case GithubScope.Env: {
+ encryptedSecrets = (
+ await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
+ repository_id: Number(integration.appId),
+ environment_name: integration.targetEnvironmentId as string
+ })
+ ).data.secrets;
+ break;
+ }
+ default: {
+ encryptedSecrets = (
+ await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
- repo: integration.app as string,
- secret_name: key
- });
+ repo: integration.app as string
+ })
+ ).data.secrets;
+ break;
+ }
+ }
+
+ for await (const encryptedSecret of encryptedSecrets) {
+ if (
+ !(encryptedSecret.name in secrets) &&
+ !(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
+ !(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
+ ) {
+ switch (integration.scope) {
+ case GithubScope.Org: {
+ await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
+ org: integration.owner as string,
+ secret_name: encryptedSecret.name
+ });
+ break;
+ }
+ case GithubScope.Env: {
+ await octokit.request(
+ "DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
+ {
+ repository_id: Number(integration.appId),
+ environment_name: integration.targetEnvironmentId as string,
+ secret_name: encryptedSecret.name
+ }
+ );
+ break;
+ }
+ default: {
+ await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
+ owner: integration.owner as string,
+ repo: integration.app as string,
+ secret_name: encryptedSecret.name
+ });
+ break;
+ }
}
- })
- );
+ }
+ }
- await Promise.all(
- Object.keys(secrets).map((key) => {
- // let encryptedSecret;
- return sodium.ready.then(async () => {
- // convert secret & base64 key to Uint8Array.
- const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
- const binsec = sodium.from_string(secrets[key].value);
+ await sodium.ready.then(async () => {
+ for await (const key of Object.keys(secrets)) {
+ // convert secret & base64 key to Uint8Array.
+ const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
+ const binsec = sodium.from_string(secrets[key].value);
- // encrypt secret using libsodium
- const encBytes = sodium.crypto_box_seal(binsec, binkey);
+ // encrypt secret using libsodium
+ const encBytes = sodium.crypto_box_seal(binsec, binkey);
- // convert encrypted Uint8Array to base64
- const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
+ // convert encrypted Uint8Array to base64
+ const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
- await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
- owner: integration.owner as string,
- repo: integration.app as string,
- secret_name: key,
- encrypted_value: encryptedSecret,
- key_id: repoPublicKey.key_id
- });
- });
- })
- );
+ switch (integration.scope) {
+ case GithubScope.Org:
+ await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
+ org: integration.owner as string,
+ secret_name: key,
+ visibility: "all",
+ encrypted_value: encryptedSecret,
+ key_id: repoPublicKey.key_id
+ });
+ break;
+ case GithubScope.Env:
+ await octokit.request(
+ "PUT /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
+ {
+ repository_id: Number(integration.appId),
+ environment_name: integration.targetEnvironmentId as string,
+ secret_name: key,
+ encrypted_value: encryptedSecret,
+ key_id: repoPublicKey.key_id
+ }
+ );
+ break;
+ default:
+ await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
+ owner: integration.owner as string,
+ repo: integration.app as string,
+ secret_name: key,
+ encrypted_value: encryptedSecret,
+ key_id: repoPublicKey.key_id
+ });
+ break;
+ }
+ }
+ });
};
/**
@@ -1229,6 +1319,22 @@ const syncSecretsRender = async ({
}
}
);
+
+ if (integration.metadata) {
+ const metadata = z.record(z.any()).parse(integration.metadata);
+ if (metadata.shouldAutoRedeploy === true) {
+ await request.post(
+ `${IntegrationUrls.RENDER_API_URL}/v1/services/${integration.appId}/deploys`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Accept-Encoding": "application/json"
+ }
+ }
+ );
+ }
+ }
};
/**
@@ -2051,16 +2157,29 @@ const syncSecretsQovery = async ({
* @param {String} obj.accessToken - access token for Terraform Cloud API
*/
const syncSecretsTerraformCloud = async ({
+ createManySecretsRawFn,
+ updateManySecretsRawFn,
integration,
secrets,
- accessToken
+ accessToken,
+ integrationDAL
}: {
- integration: TIntegrations;
- secrets: Record;
+ createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>;
+ updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>;
+ integration: TIntegrations & {
+ projectId: string;
+ environment: {
+ id: string;
+ name: string;
+ slug: string;
+ };
+ };
+ secrets: Record;
accessToken: string;
+ integrationDAL: Pick;
}) => {
// get secrets from Terraform Cloud
- const getSecretsRes = (
+ const terraformSecrets = (
await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
{
@@ -2078,9 +2197,74 @@ const syncSecretsTerraformCloud = async ({
{} as Record
);
+ const secretsToAdd: { [key: string]: string } = {};
+ const secretsToUpdate: { [key: string]: string } = {};
+
+ const metadata = z.record(z.any()).parse(integration.metadata);
+
+ Object.keys(terraformSecrets).forEach((key) => {
+ if (!integration.lastUsed) {
+ // first time using integration
+ // -> apply initial sync behavior
+ switch (metadata.initialSyncBehavior) {
+ case IntegrationInitialSyncBehavior.PREFER_TARGET: {
+ if (!(key in secrets)) {
+ secretsToAdd[key] = terraformSecrets[key].attributes.value;
+ } else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
+ secretsToUpdate[key] = terraformSecrets[key].attributes.value;
+ }
+ secrets[key] = {
+ value: terraformSecrets[key].attributes.value
+ };
+ break;
+ }
+ case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
+ if (!(key in secrets)) {
+ secrets[key] = {
+ value: terraformSecrets[key].attributes.value
+ };
+ secretsToAdd[key] = terraformSecrets[key].attributes.value;
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ } else if (!(key in secrets)) secrets[key] = null;
+ });
+
+ if (Object.keys(secretsToAdd).length) {
+ await createManySecretsRawFn({
+ projectId: integration.projectId,
+ environment: integration.environment.slug,
+ path: integration.secretPath,
+ secrets: Object.keys(secretsToAdd).map((key) => ({
+ secretName: key,
+ secretValue: secretsToAdd[key],
+ type: SecretType.Shared,
+ secretComment: ""
+ }))
+ });
+ }
+
+ if (Object.keys(secretsToUpdate).length) {
+ await updateManySecretsRawFn({
+ projectId: integration.projectId,
+ environment: integration.environment.slug,
+ path: integration.secretPath,
+ secrets: Object.keys(secretsToUpdate).map((key) => ({
+ secretName: key,
+ secretValue: secretsToUpdate[key],
+ type: SecretType.Shared,
+ secretComment: ""
+ }))
+ });
+ }
+
// create or update secrets on Terraform Cloud
for await (const key of Object.keys(secrets)) {
- if (!(key in getSecretsRes)) {
+ if (!(key in terraformSecrets)) {
// case: secret does not exist in Terraform Cloud
// -> add secret
await request.post(
@@ -2090,7 +2274,7 @@ const syncSecretsTerraformCloud = async ({
type: "vars",
attributes: {
key,
- value: secrets[key].value,
+ value: secrets[key]?.value,
category: integration.targetService
}
}
@@ -2104,17 +2288,17 @@ const syncSecretsTerraformCloud = async ({
}
);
// case: secret exists in Terraform Cloud
- } else if (secrets[key].value !== getSecretsRes[key].attributes.value) {
+ } else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
// -> update secret
await request.patch(
- `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`,
+ `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
{
data: {
type: "vars",
- id: getSecretsRes[key].id,
+ id: terraformSecrets[key].id,
attributes: {
- ...getSecretsRes[key],
- value: secrets[key].value
+ ...terraformSecrets[key],
+ value: secrets[key]?.value
}
}
},
@@ -2129,11 +2313,11 @@ const syncSecretsTerraformCloud = async ({
}
}
- for await (const key of Object.keys(getSecretsRes)) {
+ for await (const key of Object.keys(terraformSecrets)) {
if (!(key in secrets)) {
// case: delete secret
await request.delete(
- `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`,
+ `${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -2144,6 +2328,10 @@ const syncSecretsTerraformCloud = async ({
);
}
}
+
+ await integrationDAL.updateById(integration.id, {
+ lastUsed: new Date()
+ });
};
/**
@@ -3185,9 +3373,12 @@ export const syncIntegrationSecrets = async ({
break;
case Integrations.TERRAFORM_CLOUD:
await syncSecretsTerraformCloud({
+ createManySecretsRawFn,
+ updateManySecretsRawFn,
integration,
secrets,
- accessToken
+ accessToken,
+ integrationDAL
});
break;
case Integrations.HASHICORP_VAULT:
diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts
index 4a6bed75f..a3c4c84db 100644
--- a/backend/src/services/integration/integration-service.ts
+++ b/backend/src/services/integration/integration-service.ts
@@ -42,6 +42,7 @@ export const integrationServiceFactory = ({
metadata,
secretPath,
targetService,
+ actorAuthMethod,
targetServiceId,
integrationAuthId,
sourceEnvironment,
@@ -55,6 +56,7 @@ export const integrationServiceFactory = ({
actor,
actorId,
integrationAuth.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
@@ -93,6 +95,7 @@ export const integrationServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
targetEnvironment,
app,
id,
@@ -109,6 +112,7 @@ export const integrationServiceFactory = ({
actor,
actorId,
integration.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
@@ -129,7 +133,7 @@ export const integrationServiceFactory = ({
return updatedIntegration;
};
- const deleteIntegration = async ({ actorId, id, actor, actorOrgId }: TDeleteIntegrationDTO) => {
+ const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
@@ -137,16 +141,49 @@ export const integrationServiceFactory = ({
actor,
actorId,
integration.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
- const deletedIntegration = await integrationDAL.deleteById(id);
+ const deletedIntegration = await integrationDAL.transaction(async (tx) => {
+ // delete integration
+ const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
+
+ // check if there are other integrations that share the same integration auth
+ const integrations = await integrationDAL.find(
+ {
+ integrationAuthId: integration.integrationAuthId
+ },
+ tx
+ );
+
+ if (integrations.length === 0) {
+ // no other integration shares the same integration auth
+ // -> delete the integration auth
+ await integrationAuthDAL.deleteById(integration.integrationAuthId, tx);
+ }
+
+ return deletedIntegrationResult;
+ });
+
return { ...integration, ...deletedIntegration };
};
- const listIntegrationByProject = async ({ actor, actorId, actorOrgId, projectId }: TProjectPermission) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const listIntegrationByProject = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TProjectPermission) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const integrations = await integrationDAL.findByProjectId(projectId);
diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts
index 8f54c4fdb..56ea46350 100644
--- a/backend/src/services/integration/integration-types.ts
+++ b/backend/src/services/integration/integration-types.ts
@@ -22,6 +22,11 @@ export type TCreateIntegrationDTO = {
labelName: string;
labelValue: string;
};
+ secretAWSTag?: {
+ key: string;
+ value: string;
+ }[];
+ kmsKeyId?: string;
};
} & Omit;
diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts
index fb8a57440..70c54ff18 100644
--- a/backend/src/services/org/org-role-service.ts
+++ b/backend/src/services/org/org-role-service.ts
@@ -12,6 +12,7 @@ import {
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError } from "@app/lib/errors";
+import { ActorAuthMethod } from "../auth/auth-type";
import { TOrgRoleDALFactory } from "./org-role-dal";
type TOrgRoleServiceFactoryDep = {
@@ -26,9 +27,10 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
userId: string,
orgId: string,
data: Omit,
- actorOrgId?: string
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
@@ -45,9 +47,10 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
orgId: string,
roleId: string,
data: Omit,
- actorOrgId?: string
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
if (data?.slug) {
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
@@ -62,8 +65,14 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
return updatedRole;
};
- const deleteRole = async (userId: string, orgId: string, roleId: string, actorOrgId?: string) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const deleteRole = async (
+ userId: string,
+ orgId: string,
+ roleId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
@@ -71,8 +80,13 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
return deletedRole;
};
- const listRoles = async (userId: string, orgId: string, actorOrgId?: string) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const listRoles = async (
+ userId: string,
+ orgId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
const customRoles = await orgRoleDAL.find({ orgId });
const roles = [
@@ -115,8 +129,18 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
return roles;
};
- const getUserPermission = async (userId: string, orgId: string, actorOrgId?: string) => {
- const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const getUserPermission = async (
+ userId: string,
+ orgId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission, membership } = await permissionService.getUserOrgPermission(
+ userId,
+ orgId,
+ actorAuthMethod,
+ actorOrgId
+ );
return { permissions: packRules(permission.rules), membership };
};
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index db6d9654d..c03fe1748 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -6,6 +6,7 @@ import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
+import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -18,7 +19,7 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
-import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
+import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal";
@@ -34,6 +35,7 @@ import {
TDeleteOrgMembershipDTO,
TFindAllWorkspacesDTO,
TFindOrgMembersByEmailDTO,
+ TGetOrgGroupsDTO,
TInviteUserToOrgDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
@@ -45,6 +47,7 @@ type TOrgServiceFactoryDep = {
orgBotDAL: TOrgBotDALFactory;
orgRoleDAL: TOrgRoleDALFactory;
userDAL: TUserDALFactory;
+ groupDAL: TGroupDALFactory;
projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick;
projectKeyDAL: Pick;
@@ -64,6 +67,7 @@ export type TOrgServiceFactory = ReturnType;
export const orgServiceFactory = ({
orgDAL,
userDAL,
+ groupDAL,
orgRoleDAL,
incidentContactDAL,
permissionService,
@@ -79,8 +83,13 @@ export const orgServiceFactory = ({
/*
* Get organization details by the organization id
* */
- const findOrganizationById = async (userId: string, orgId: string, actorOrgId?: string) => {
- await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const findOrganizationById = async (
+ userId: string,
+ orgId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
return org;
@@ -95,16 +104,35 @@ export const orgServiceFactory = ({
/*
* Get all workspace members
* */
- const findAllOrgMembers = async (userId: string, orgId: string, actorOrgId?: string) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const findAllOrgMembers = async (
+ userId: string,
+ orgId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const members = await orgDAL.findAllOrgMembers(orgId);
return members;
};
- const findOrgMembersByUsername = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
+ const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
+ const groups = await groupDAL.findByOrgId(orgId);
+ return groups;
+ };
+
+ const findOrgMembersByUsername = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ orgId,
+ emails
+ }: TFindOrgMembersByEmailDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const members = await orgDAL.findOrgMembersByUsername(orgId, emails);
@@ -112,8 +140,8 @@ export const orgServiceFactory = ({
return members;
};
- const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const findAllWorkspaces = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TFindAllWorkspacesDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
@@ -193,10 +221,11 @@ export const orgServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
orgId,
data: { name, slug, authEnforced, scimEnabled }
}: TUpdateOrgDTO) => {
- const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
const plan = await licenseService.getPlan(orgId);
@@ -309,8 +338,13 @@ export const orgServiceFactory = ({
/*
* Delete organization by id
* */
- const deleteOrganizationById = async (userId: string, orgId: string, actorOrgId?: string) => {
- const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const deleteOrganizationById = async (
+ userId: string,
+ orgId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin)
throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" });
@@ -324,8 +358,15 @@ export const orgServiceFactory = ({
* Org membership management
* Not another service because it has close ties with how an org works doesn't make sense to seperate them
* */
- const updateOrgMembership = async ({ role, orgId, userId, membershipId, actorOrgId }: TUpdateOrgMembershipDTO) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const updateOrgMembership = async ({
+ role,
+ orgId,
+ userId,
+ membershipId,
+ actorAuthMethod,
+ actorOrgId
+ }: TUpdateOrgMembershipDTO) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
@@ -355,8 +396,14 @@ export const orgServiceFactory = ({
/*
* Invite user to organization
*/
- const inviteUserToOrganization = async ({ orgId, userId, inviteeEmail, actorOrgId }: TInviteUserToOrgDTO) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const inviteUserToOrganization = async ({
+ orgId,
+ userId,
+ inviteeEmail,
+ actorAuthMethod,
+ actorOrgId
+ }: TInviteUserToOrgDTO) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const org = await orgDAL.findOrgById(orgId);
@@ -515,8 +562,14 @@ export const orgServiceFactory = ({
return { token, user };
};
- const deleteOrgMembership = async ({ orgId, userId, membershipId, actorOrgId }: TDeleteOrgMembershipDTO) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const deleteOrgMembership = async ({
+ orgId,
+ userId,
+ membershipId,
+ actorAuthMethod,
+ actorOrgId
+ }: TDeleteOrgMembershipDTO) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const deletedMembership = await orgDAL.transaction(async (tx) => {
@@ -568,15 +621,26 @@ export const orgServiceFactory = ({
/*
* CRUD operations of incident contacts
* */
- const findIncidentContacts = async (userId: string, orgId: string, actorOrgId?: string) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const findIncidentContacts = async (
+ userId: string,
+ orgId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
const incidentContacts = await incidentContactDAL.findByOrgId(orgId);
return incidentContacts;
};
- const createIncidentContact = async (userId: string, orgId: string, email: string, actorOrgId?: string) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const createIncidentContact = async (
+ userId: string,
+ orgId: string,
+ email: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
if (doesIncidentContactExist) {
@@ -590,8 +654,14 @@ export const orgServiceFactory = ({
return incidentContact;
};
- const deleteIncidentContact = async (userId: string, orgId: string, id: string, actorOrgId?: string) => {
- const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
+ const deleteIncidentContact = async (
+ userId: string,
+ orgId: string,
+ id: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
const incidentContact = await incidentContactDAL.deleteById(id, orgId);
@@ -615,6 +685,7 @@ export const orgServiceFactory = ({
// incident contacts
findIncidentContacts,
createIncidentContact,
- deleteIncidentContact
+ deleteIncidentContact,
+ getOrgGroups
};
};
diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts
index bd8fe2e95..0efc7ffe1 100644
--- a/backend/src/services/org/org-types.ts
+++ b/backend/src/services/org/org-types.ts
@@ -1,26 +1,29 @@
import { TOrgPermission } from "@app/lib/types";
-import { ActorType } from "../auth/auth-type";
+import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type TUpdateOrgMembershipDTO = {
userId: string;
orgId: string;
membershipId: string;
role: string;
- actorOrgId?: string;
+ actorOrgId: string | undefined;
+ actorAuthMethod: ActorAuthMethod;
};
export type TDeleteOrgMembershipDTO = {
userId: string;
orgId: string;
membershipId: string;
- actorOrgId?: string;
+ actorOrgId: string | undefined;
+ actorAuthMethod: ActorAuthMethod;
};
export type TInviteUserToOrgDTO = {
userId: string;
orgId: string;
- actorOrgId?: string;
+ actorOrgId: string | undefined;
+ actorAuthMethod: ActorAuthMethod;
inviteeEmail: string;
};
@@ -32,7 +35,9 @@ export type TVerifyUserToOrgDTO = {
export type TFindOrgMembersByEmailDTO = {
actor: ActorType;
+ actorOrgId: string | undefined;
actorId: string;
+ actorAuthMethod: ActorAuthMethod;
orgId: string;
emails: string[];
};
@@ -40,10 +45,13 @@ export type TFindOrgMembersByEmailDTO = {
export type TFindAllWorkspacesDTO = {
actor: ActorType;
actorId: string;
- actorOrgId?: string;
+ actorOrgId: string | undefined;
+ actorAuthMethod: ActorAuthMethod;
orgId: string;
};
export type TUpdateOrgDTO = {
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
} & TOrgPermission;
+
+export type TGetOrgGroupsDTO = TOrgPermission;
diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts
index 6e281e69d..23667ef67 100644
--- a/backend/src/services/project-bot/project-bot-service.ts
+++ b/backend/src/services/project-bot/project-bot-service.ts
@@ -37,10 +37,17 @@ export const projectBotServiceFactory = ({
projectId,
actorOrgId,
privateKey,
+ actorAuthMethod,
botKey,
publicKey
}: TFindBotByProjectIdDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const bot = await projectBotDAL.transaction(async (tx) => {
@@ -88,11 +95,25 @@ export const projectBotServiceFactory = ({
}
};
- const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => {
+ const setBotActiveState = async ({
+ actor,
+ botId,
+ botKey,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ isActive
+ }: TSetActiveStateDTO) => {
const bot = await projectBotDAL.findById(botId);
if (!bot) throw new BadRequestError({ message: "Bot not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, bot.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ bot.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const project = await projectBotDAL.findProjectByBotId(botId);
diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts
index 6ebb3a3d6..2acda33c0 100644
--- a/backend/src/services/project-env/project-env-service.ts
+++ b/backend/src/services/project-env/project-env-service.ts
@@ -27,8 +27,22 @@ export const projectEnvServiceFactory = ({
projectDAL,
folderDAL
}: TProjectEnvServiceFactoryDep) => {
- const createEnvironment = async ({ projectId, actorId, actor, actorOrgId, name, slug }: TCreateEnvDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const createEnvironment = async ({
+ projectId,
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ name,
+ slug
+ }: TCreateEnvDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
const envs = await projectEnvDAL.find({ projectId });
@@ -65,11 +79,18 @@ export const projectEnvServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
name,
id,
position
}: TUpdateEnvDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
@@ -94,8 +115,14 @@ export const projectEnvServiceFactory = ({
return { environment: env, old: oldEnv };
};
- const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, id }: TDeleteEnvDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
const env = await projectEnvDAL.transaction(async (tx) => {
diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts
index fa77760a4..70c8365ee 100644
--- a/backend/src/services/project-key/project-key-service.ts
+++ b/backend/src/services/project-key/project-key-service.ts
@@ -26,11 +26,18 @@ export const projectKeyServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
projectId,
nonce,
encryptedKey
}: TUploadProjectKeyDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const receiverMembership = await projectMembershipDAL.findOne({
@@ -46,14 +53,32 @@ export const projectKeyServiceFactory = ({
await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId });
};
- const getLatestProjectKey = async ({ actorId, projectId, actor, actorOrgId }: TGetLatestProjectKeyDTO) => {
- await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getLatestProjectKey = async ({
+ actorId,
+ projectId,
+ actor,
+ actorOrgId,
+ actorAuthMethod
+ }: TGetLatestProjectKeyDTO) => {
+ await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
return latestKey;
};
- const getProjectPublicKeys = async ({ actor, actorId, actorOrgId, projectId }: TGetLatestProjectKeyDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getProjectPublicKeys = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TGetLatestProjectKeyDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return projectKeyDAL.findAllProjectUserPubKeys(projectId);
};
diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts
index 1f751ee89..6d148d03b 100644
--- a/backend/src/services/project-membership/project-membership-service.ts
+++ b/backend/src/services/project-membership/project-membership-service.ts
@@ -17,6 +17,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
+import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
@@ -45,6 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
projectMembershipDAL: TProjectMembershipDALFactory;
projectUserMembershipRoleDAL: Pick;
userDAL: Pick;
+ userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectRoleDAL: Pick;
orgDAL: Pick;
projectDAL: Pick;
@@ -63,12 +65,25 @@ export const projectMembershipServiceFactory = ({
projectBotDAL,
orgDAL,
userDAL,
+ userGroupMembershipDAL,
projectDAL,
projectKeyDAL,
licenseService
}: TProjectMembershipServiceFactoryDep) => {
- const getProjectMemberships = async ({ actorId, actor, actorOrgId, projectId }: TGetProjectMembershipDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getProjectMemberships = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TGetProjectMembershipDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
return projectMembershipDAL.findAllProjectMembers(projectId);
@@ -79,13 +94,20 @@ export const projectMembershipServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
members,
sendEmails = true
}: TAddUsersToWorkspaceDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findMembership({
orgId: project.orgId,
@@ -101,12 +123,18 @@ export const projectMembershipServiceFactory = ({
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
+ const userIdsToExcludeForProjectKeyAddition = new Set(
+ await userGroupMembershipDAL.findUserGroupMembershipsInProject(
+ orgMembers.map(({ username }) => username),
+ projectId
+ )
+ );
+
await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
projectId,
- userId: userId as string,
- role: ProjectMembershipRole.Member
+ userId: userId as string
})),
tx
);
@@ -116,13 +144,15 @@ export const projectMembershipServiceFactory = ({
);
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
- orgMembers.map(({ userId, id }) => ({
- encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
- nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
- senderId: actorId,
- receiverId: userId as string,
- projectId
- })),
+ orgMembers
+ .filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string))
+ .map(({ userId, id }) => ({
+ encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
+ nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
+ senderId: actorId,
+ receiverId: userId as string,
+ projectId
+ })),
tx
);
});
@@ -145,7 +175,9 @@ export const projectMembershipServiceFactory = ({
const addUsersToProjectNonE2EE = async ({
projectId,
actorId,
+ actorAuthMethod,
actor,
+ actorOrgId,
emails,
usernames,
sendEmails = true
@@ -157,7 +189,13 @@ export const projectMembershipServiceFactory = ({
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
}
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const usernamesAndEmails = [...emails, ...usernames];
@@ -220,12 +258,15 @@ export const projectMembershipServiceFactory = ({
const members: TProjectMemberships[] = [];
+ const userIdsToExcludeForProjectKeyAddition = new Set(
+ await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
+ );
+
await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
- userId: user.id,
- role: ProjectMembershipRole.Member
+ userId: user.id
})),
tx
);
@@ -238,13 +279,15 @@ export const projectMembershipServiceFactory = ({
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
- orgMembers.map(({ user, id }) => ({
- encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
- nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
- senderId: ghostUser.id,
- receiverId: user.id,
- projectId
- })),
+ orgMembers
+ .filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
+ .map(({ user, id }) => ({
+ encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
+ nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
+ senderId: ghostUser.id,
+ receiverId: user.id,
+ projectId
+ })),
tx
);
});
@@ -273,11 +316,18 @@ export const projectMembershipServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
projectId,
membershipId,
roles
}: TUpdateProjectMembershipDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
@@ -294,7 +344,7 @@ export const projectMembershipServiceFactory = ({
);
const hasCustomRole = Boolean(customInputRoles.length);
if (hasCustomRole) {
- const plan = await licenseService.getPlan(actorOrgId as string);
+ const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.rbac)
throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
@@ -310,7 +360,7 @@ export const projectMembershipServiceFactory = ({
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
- const santiziedProjectMembershipRoles = roles.map((inputRole) => {
+ const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
@@ -336,7 +386,7 @@ export const projectMembershipServiceFactory = ({
const updatedRoles = await projectMembershipDAL.transaction(async (tx) => {
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
- return projectUserMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx);
+ return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
return updatedRoles;
@@ -347,10 +397,17 @@ export const projectMembershipServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
projectId,
membershipId
}: TDeleteProjectMembershipOldDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
const member = await userDAL.findUserByProjectMembershipId(membershipId);
@@ -374,11 +431,18 @@ export const projectMembershipServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
projectId,
emails,
usernames
}: TDeleteProjectMembershipsDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
const project = await projectDAL.findById(projectId);
@@ -410,6 +474,10 @@ export const projectMembershipServiceFactory = ({
});
}
+ const userIdsToExcludeFromProjectKeyRemoval = new Set(
+ await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
+ );
+
const memberships = await projectMembershipDAL.transaction(async (tx) => {
const deletedMemberships = await projectMembershipDAL.delete(
{
@@ -421,11 +489,15 @@ export const projectMembershipServiceFactory = ({
tx
);
+ // delete project keys belonging to users that are not part of any other groups in the project
await projectKeyDAL.delete(
{
projectId,
$in: {
- receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean)
+ receiverId: projectMembers
+ .filter(({ user }) => !userIdsToExcludeFromProjectKeyRemoval.has(user.id))
+ .map(({ user }) => user.id)
+ .filter(Boolean)
}
},
tx
diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts
index b45a6e8a5..831af3200 100644
--- a/backend/src/services/project-role/project-role-service.ts
+++ b/backend/src/services/project-role/project-role-service.ts
@@ -13,25 +13,41 @@ import {
} from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
-import { ActorType } from "../auth/auth-type";
+import { ActorAuthMethod, ActorType } from "../auth/auth-type";
+import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
+import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "./project-role-dal";
type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory;
permissionService: Pick;
+ identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
+ projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
};
export type TProjectRoleServiceFactory = ReturnType;
-export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: TProjectRoleServiceFactoryDep) => {
+export const projectRoleServiceFactory = ({
+ projectRoleDAL,
+ permissionService,
+ identityProjectMembershipRoleDAL,
+ projectUserMembershipRoleDAL
+}: TProjectRoleServiceFactoryDep) => {
const createRole = async (
actor: ActorType,
actorId: string,
projectId: string,
data: Omit,
- actorOrgId?: string
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
@@ -49,9 +65,16 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
projectId: string,
roleId: string,
data: Omit,
- actorOrgId?: string
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
if (data?.slug) {
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
@@ -71,18 +94,54 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
actorId: string,
projectId: string,
roleId: string,
- actorOrgId?: string
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
+
+ const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId });
+ const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId });
+
+ if (identityRole) {
+ throw new BadRequestError({
+ message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.",
+ name: "Delete role"
+ });
+ }
+ if (projectUserRole) {
+ throw new BadRequestError({
+ message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.",
+ name: "Delete role"
+ });
+ }
+
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
- if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
+ if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" });
return deletedRole;
};
- const listRoles = async (actor: ActorType, actorId: string, projectId: string, actorOrgId?: string) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const listRoles = async (
+ actor: ActorType,
+ actorId: string,
+ projectId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
const customRoles = await projectRoleDAL.find({ projectId });
const roles = [
@@ -135,8 +194,18 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }:
return roles;
};
- const getUserPermission = async (userId: string, projectId: string, actorOrgId?: string) => {
- const { permission, membership } = await permissionService.getUserProjectPermission(userId, projectId, actorOrgId);
+ const getUserPermission = async (
+ userId: string,
+ projectId: string,
+ actorAuthMethod: ActorAuthMethod,
+ actorOrgId: string | undefined
+ ) => {
+ const { permission, membership } = await permissionService.getUserProjectPermission(
+ userId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
return { permissions: packRules(permission.rules), membership };
};
diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts
index 7d0826e12..42cc54393 100644
--- a/backend/src/services/project/project-dal.ts
+++ b/backend/src/services/project/project-dal.ts
@@ -5,6 +5,8 @@ import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProje
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
+import { Filter, ProjectFilterType } from "./project-types";
+
export type TProjectDALFactory = ReturnType;
export const projectDALFactory = (db: TDbClient) => {
@@ -28,8 +30,33 @@ export const projectDALFactory = (db: TDbClient) => {
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
+ const groups: string[] = await db(TableName.UserGroupMembership)
+ .where({ userId })
+ .select(selectAllTableCols(TableName.UserGroupMembership))
+ .pluck("groupId");
+
+ const groupWorkspaces = await db(TableName.GroupProjectMembership)
+ .whereIn("groupId", groups)
+ .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
+ .whereNotIn(
+ `${TableName.Project}.id`,
+ workspaces.map(({ id }) => id)
+ )
+ .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
+ .select(
+ selectAllTableCols(TableName.Project),
+ db.ref("id").withSchema(TableName.Project).as("_id"),
+ db.ref("id").withSchema(TableName.Environment).as("envId"),
+ db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
+ db.ref("name").withSchema(TableName.Environment).as("envName")
+ )
+ .orderBy([
+ { column: `${TableName.Project}.name`, order: "asc" },
+ { column: `${TableName.Environment}.position`, order: "asc" }
+ ]);
+
const nestedWorkspaces = sqlNestRelationships({
- data: workspaces,
+ data: workspaces.concat(groupWorkspaces),
key: "id",
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
childrenMapper: [
@@ -124,13 +151,11 @@ export const projectDALFactory = (db: TDbClient) => {
const findProjectById = async (id: string) => {
try {
- const workspaces = await db(TableName.ProjectMembership)
+ const workspaces = await db(TableName.Project)
.where(`${TableName.Project}.id`, id)
- .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
- .join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
+ .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
selectAllTableCols(TableName.Project),
- db.ref("id").withSchema(TableName.Project).as("_id"),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
@@ -139,10 +164,11 @@ export const projectDALFactory = (db: TDbClient) => {
{ column: `${TableName.Project}.name`, order: "asc" },
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
- return sqlNestRelationships({
+
+ const project = sqlNestRelationships({
data: workspaces,
key: "id",
- parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
+ parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }),
childrenMapper: [
{
key: "envId",
@@ -155,11 +181,88 @@ export const projectDALFactory = (db: TDbClient) => {
}
]
})?.[0];
+
+ if (!project) {
+ throw new BadRequestError({ message: "Project not found" });
+ }
+
+ return project;
} catch (error) {
throw new DatabaseError({ error, name: "Find all projects" });
}
};
+ const findProjectBySlug = async (slug: string, orgId: string | undefined) => {
+ try {
+ if (!orgId) {
+ throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
+ }
+
+ const projects = await db(TableName.Project)
+ .where(`${TableName.Project}.slug`, slug)
+ .where(`${TableName.Project}.orgId`, orgId)
+ .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
+ .select(
+ selectAllTableCols(TableName.Project),
+ db.ref("id").withSchema(TableName.Environment).as("envId"),
+ db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
+ db.ref("name").withSchema(TableName.Environment).as("envName")
+ )
+ .orderBy([
+ { column: `${TableName.Project}.name`, order: "asc" },
+ { column: `${TableName.Environment}.position`, order: "asc" }
+ ]);
+
+ const project = sqlNestRelationships({
+ data: projects,
+ key: "id",
+ parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }),
+ childrenMapper: [
+ {
+ key: "envId",
+ label: "environments" as const,
+ mapper: ({ envId, envSlug, envName: name }) => ({
+ id: envId,
+ slug: envSlug,
+ name
+ })
+ }
+ ]
+ })?.[0];
+
+ if (!project) {
+ throw new BadRequestError({ message: "Project not found" });
+ }
+
+ return project;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Find project by slug" });
+ }
+ };
+
+ const findProjectByFilter = async (filter: Filter) => {
+ try {
+ if (filter.type === ProjectFilterType.ID) {
+ return await findProjectById(filter.projectId);
+ }
+ if (filter.type === ProjectFilterType.SLUG) {
+ if (!filter.orgId) {
+ throw new BadRequestError({
+ message: "Organization ID is required when querying with slugs"
+ });
+ }
+
+ return await findProjectBySlug(filter.slug, filter.orgId);
+ }
+ throw new BadRequestError({ message: "Invalid filter type" });
+ } catch (error) {
+ if (error instanceof BadRequestError) {
+ throw error;
+ }
+ throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` });
+ }
+ };
+
const checkProjectUpgradeStatus = async (projectId: string) => {
const project = await projectOrm.findById(projectId);
const upgradeInProgress =
@@ -179,6 +282,8 @@ export const projectDALFactory = (db: TDbClient) => {
findAllProjectsByIdentity,
findProjectGhostUser,
findProjectById,
+ findProjectByFilter,
+ findProjectBySlug,
checkProjectUpgradeStatus
};
};
diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts
index 4431855ce..81ecd6da1 100644
--- a/backend/src/services/project/project-queue.ts
+++ b/backend/src/services/project/project-queue.ts
@@ -232,8 +232,7 @@ export const projectQueueFactory = ({
const projectMembership = await projectMembershipDAL.create(
{
projectId: project.id,
- userId: ghostUser.user.id,
- role: ProjectMembershipRole.Admin
+ userId: ghostUser.user.id
},
tx
);
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index 873a7d36f..008dac593 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -1,11 +1,12 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
-import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
+import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
@@ -18,6 +19,7 @@ import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
+import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -34,7 +36,9 @@ import {
TCreateProjectDTO,
TDeleteProjectDTO,
TGetProjectDTO,
+ TToggleProjectAutoCapitalizationDTO,
TUpdateProjectDTO,
+ TUpdateProjectNameDTO,
TUpgradeProjectDTO
} from "./project-types";
@@ -61,6 +65,8 @@ type TProjectServiceFactoryDep = {
permissionService: TPermissionServiceFactory;
orgService: Pick;
licenseService: Pick;
+ orgDAL: Pick;
+ keyStore: Pick;
};
export type TProjectServiceFactory = ReturnType;
@@ -70,6 +76,7 @@ export const projectServiceFactory = ({
projectQueue,
projectKeyDAL,
permissionService,
+ orgDAL,
userDAL,
folderDAL,
orgService,
@@ -81,16 +88,27 @@ export const projectServiceFactory = ({
projectEnvDAL,
licenseService,
projectUserMembershipRoleDAL,
- identityProjectMembershipRoleDAL
+ identityProjectMembershipRoleDAL,
+ keyStore
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
* */
- const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName, slug }: TCreateProjectDTO) => {
+ const createProject = async ({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ workspaceName,
+ slug: projectSlug
+ }: TCreateProjectDTO) => {
+ const organization = await orgDAL.findOne({ id: actorOrgId });
+
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
actor,
actorId,
- orgId,
+ organization.id,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
@@ -98,7 +116,7 @@ export const projectServiceFactory = ({
const appCfg = getConfig();
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
- const plan = await licenseService.getPlan(orgId);
+ const plan = await licenseService.getPlan(organization.id);
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
// case: limit imposed on number of workspaces allowed
// case: number of workspaces used exceeds the number of workspaces allowed
@@ -108,13 +126,13 @@ export const projectServiceFactory = ({
}
const results = await projectDAL.transaction(async (tx) => {
- const ghostUser = await orgService.addGhostUser(orgId, tx);
+ const ghostUser = await orgService.addGhostUser(organization.id, tx);
const project = await projectDAL.create(
{
name: workspaceName,
- orgId,
- slug: slug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
+ orgId: organization.id,
+ slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
version: ProjectVersion.V2
},
tx
@@ -123,8 +141,7 @@ export const projectServiceFactory = ({
const projectMembership = await projectMembershipDAL.create(
{
userId: ghostUser.user.id,
- projectId: project.id,
- role: ProjectMembershipRole.Admin
+ projectId: project.id
},
tx
);
@@ -226,8 +243,7 @@ export const projectServiceFactory = ({
const userProjectMembership = await projectMembershipDAL.create(
{
projectId: project.id,
- userId: user.id,
- role: projectAdmin.projectRole
+ userId: user.id
},
tx
);
@@ -269,10 +285,11 @@ export const projectServiceFactory = ({
// Get the role permission for the identity
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
- ProjectMembershipRole.Admin,
- orgId
+ OrgMembershipRole.Member,
+ organization.id
);
+ // Identity has to be at least a member in order to create projects
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege)
throw new ForbiddenRequestError({
@@ -283,9 +300,7 @@ export const projectServiceFactory = ({
const identityProjectMembership = await identityProjectDAL.create(
{
identityId: actorId,
- projectId: project.id,
- role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
- roleId: customRole?.id
+ projectId: project.id
},
tx
);
@@ -307,25 +322,35 @@ export const projectServiceFactory = ({
};
});
+ await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`);
return results;
};
- const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const deleteProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, filter }: TDeleteProjectDTO) => {
+ const project = await projectDAL.findProjectByFilter(filter);
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
const deletedProject = await projectDAL.transaction(async (tx) => {
- const project = await projectDAL.deleteById(projectId, tx);
- const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(projectId).catch(() => null);
+ const delProject = await projectDAL.deleteById(project.id, tx);
+ const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id).catch(() => null);
// Delete the org membership for the ghost user if it's found.
if (projectGhostUser) {
await userDAL.deleteById(projectGhostUser.id, tx);
}
- return project;
+ return delProject;
});
+ await keyStore.deleteItem(`infisical-cloud-plan-${actorOrgId}`);
return deletedProject;
};
@@ -334,16 +359,26 @@ export const projectServiceFactory = ({
return workspaces;
};
- const getAProject = async ({ actorId, actorOrgId, projectId, actor }: TGetProjectDTO) => {
- await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
- return projectDAL.findProjectById(projectId);
+ const getAProject = async ({ actorId, actorOrgId, actorAuthMethod, filter, actor }: TGetProjectDTO) => {
+ const project = await projectDAL.findProjectByFilter(filter);
+
+ await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId);
+ return project;
};
- const updateProject = async ({ projectId, actor, actorId, actorOrgId, update }: TUpdateProjectDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const updateProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, update, filter }: TUpdateProjectDTO) => {
+ const project = await projectDAL.findProjectByFilter(filter);
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
- const updatedProject = await projectDAL.updateById(projectId, {
+ const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
autoCapitalization: update.autoCapitalization
});
@@ -355,25 +390,58 @@ export const projectServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
autoCapitalization
- }: TGetProjectDTO & { autoCapitalization: boolean }) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ }: TToggleProjectAutoCapitalizationDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization });
return updatedProject;
};
- const updateName = async ({ projectId, actor, actorId, actorOrgId, name }: TGetProjectDTO & { name: string }) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const updateName = async ({
+ projectId,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ name
+ }: TUpdateProjectNameDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { name });
return updatedProject;
};
- const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
- const { permission, hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId);
+ const upgradeProject = async ({
+ projectId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId,
+ userPrivateKey
+ }: TUpgradeProjectDTO) => {
+ const { permission, hasRole } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
@@ -397,8 +465,20 @@ export const projectServiceFactory = ({
});
};
- const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
+ const getProjectUpgradeStatus = async ({
+ projectId,
+ actor,
+ actorAuthMethod,
+ actorOrgId,
+ actorId
+ }: TProjectPermission) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
const project = await projectDAL.findProjectById(projectId);
diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts
index 3843450c2..dcd424e18 100644
--- a/backend/src/services/project/project-types.ts
+++ b/backend/src/services/project/project-types.ts
@@ -1,37 +1,66 @@
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
-import { ActorType } from "../auth/auth-type";
+import { ActorAuthMethod, ActorType } from "../auth/auth-type";
+
+export enum ProjectFilterType {
+ ID = "id",
+ SLUG = "slug"
+}
+
+export type Filter =
+ | {
+ type: ProjectFilterType.ID;
+ projectId: string;
+ }
+ | {
+ type: ProjectFilterType.SLUG;
+ slug: string;
+ orgId: string | undefined;
+ };
export type TCreateProjectDTO = {
actor: ActorType;
+ actorAuthMethod: ActorAuthMethod;
actorId: string;
actorOrgId?: string;
- orgId: string;
workspaceName: string;
slug?: string;
};
-export type TDeleteProjectDTO = {
+export type TDeleteProjectBySlugDTO = {
+ slug: string;
actor: ActorType;
actorId: string;
- actorOrgId?: string;
- projectId: string;
+ actorOrgId: string | undefined;
};
export type TGetProjectDTO = {
- actor: ActorType;
- actorId: string;
- actorOrgId?: string;
- projectId: string;
-};
+ filter: Filter;
+} & Omit;
+
+export type TToggleProjectAutoCapitalizationDTO = {
+ autoCapitalization: boolean;
+} & TProjectPermission;
+
+export type TUpdateProjectNameDTO = {
+ name: string;
+} & TProjectPermission;
export type TUpdateProjectDTO = {
+ filter: Filter;
update: {
name?: string;
autoCapitalization?: boolean;
};
-} & TProjectPermission;
+} & Omit;
+
+export type TDeleteProjectDTO = {
+ filter: Filter;
+ actor: ActorType;
+ actorId: string;
+ actorOrgId: string | undefined;
+} & Omit;
export type TUpgradeProjectDTO = {
userPrivateKey: string;
diff --git a/backend/src/services/secret-blind-index/secret-blind-index-service.ts b/backend/src/services/secret-blind-index/secret-blind-index-service.ts
index b681266fd..bf2728e95 100644
--- a/backend/src/services/secret-blind-index/secret-blind-index-service.ts
+++ b/backend/src/services/secret-blind-index/secret-blind-index-service.ts
@@ -28,16 +28,29 @@ export const secretBlindIndexServiceFactory = ({
actor,
projectId,
actorId,
+ actorAuthMethod,
actorOrgId
}: TGetProjectBlindIndexStatusDTO) => {
- await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const secretCount = await secretBlindIndexDAL.countOfSecretsWithNullSecretBlindIndex(projectId);
return Number(secretCount);
};
- const getProjectSecrets = async ({ projectId, actorId, actor }: TGetProjectSecretsDTO) => {
- const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId);
+ const getProjectSecrets = async ({
+ projectId,
+ actorId,
+ actorAuthMethod,
+ actorOrgId,
+ actor
+ }: TGetProjectSecretsDTO) => {
+ const { hasRole } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
if (!hasRole(ProjectMembershipRole.Admin)) {
throw new UnauthorizedError({ message: "User must be admin" });
}
@@ -50,10 +63,17 @@ export const secretBlindIndexServiceFactory = ({
projectId,
actor,
actorId,
+ actorAuthMethod,
actorOrgId,
secretsToUpdate
}: TUpdateProjectSecretNameDTO) => {
- const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { hasRole } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
if (!hasRole(ProjectMembershipRole.Admin)) {
throw new UnauthorizedError({ message: "User must be admin" });
}
diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts
index 023d039ca..b3147d1fa 100644
--- a/backend/src/services/secret-folder/secret-folder-dal.ts
+++ b/backend/src/services/secret-folder/secret-folder-dal.ts
@@ -170,7 +170,8 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
// if the given folder id is root folder id then intial path is set as / instead of /root
// if not root folder the path here will be /
path: db.raw(`CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))`),
- child: db.raw("NULL::uuid")
+ child: db.raw("NULL::uuid"),
+ environmentSlug: `${TableName.Environment}.slug`
})
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where({ projectId })
@@ -190,14 +191,15 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
ELSE CONCAT('/', secret_folders.name)
END, parent.path )`
),
- child: db.raw("COALESCE(parent.child, parent.id)")
+ child: db.raw("COALESCE(parent.child, parent.id)"),
+ environmentSlug: "parent.environmentSlug"
})
.from(TableName.SecretFolder)
.join("parent", "parent.parentId", `${TableName.SecretFolder}.id`)
);
})
.select("*")
- .from("parent");
+ .from("parent");
export type TSecretFolderDALFactory = ReturnType;
// never change this. If u do write a migration for it
@@ -257,10 +259,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => {
try {
const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds);
+
const rootFolders = groupBy(
folders.filter(({ parentId }) => parentId === null),
(i) => i.child || i.id // root condition then child and parent will null
);
+
return folderIds.map((folderId) => rootFolders[folderId]?.[0]);
} catch (error) {
throw new DatabaseError({ error, name: "Find by secret path" });
diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts
index 26c1c1f4f..c925d2587 100644
--- a/backend/src/services/secret-folder/secret-folder-service.ts
+++ b/backend/src/services/secret-folder/secret-folder-service.ts
@@ -34,12 +34,19 @@ export const secretFolderServiceFactory = ({
projectId,
actor,
actorId,
+ actorAuthMethod,
actorOrgId,
name,
environment,
path: secretPath
}: TCreateFolderDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@@ -114,12 +121,19 @@ export const secretFolderServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
name,
environment,
path: secretPath,
id
}: TUpdateFolderDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@@ -162,11 +176,18 @@ export const secretFolderServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
environment,
path: secretPath,
idOrName
}: TDeleteFolderDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@@ -196,12 +217,13 @@ export const secretFolderServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
environment,
path: secretPath
}: TGetFolderDTO) => {
// folder list is allowed to be read by anyone
// permission to check does user has access
- await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "get folders" });
@@ -210,6 +232,7 @@ export const secretFolderServiceFactory = ({
if (!parentFolder) return [];
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
+
return folders;
};
diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts
index f9c6f1be7..aa45d410d 100644
--- a/backend/src/services/secret-import/secret-import-dal.ts
+++ b/backend/src/services/secret-import/secret-import-dal.ts
@@ -49,7 +49,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
}
};
- const find = async (filter: Partial, tx?: Knex) => {
+ const find = async (filter: Partial, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretImport)
.where(filter)
@@ -70,9 +70,31 @@ export const secretImportDALFactory = (db: TDbClient) => {
}
};
+ const findByFolderIds = async (folderIds: string[], tx?: Knex) => {
+ try {
+ const docs = await (tx || db)(TableName.SecretImport)
+ .whereIn("folderId", folderIds)
+ .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
+ .select(
+ db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
+ db.ref("slug").withSchema(TableName.Environment),
+ db.ref("name").withSchema(TableName.Environment),
+ db.ref("id").withSchema(TableName.Environment).as("envId")
+ )
+ .orderBy("position", "asc");
+ return docs.map(({ envId, slug, name, ...el }) => ({
+ ...el,
+ importEnv: { id: envId, slug, name }
+ }));
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Find secret imports" });
+ }
+ };
+
return {
...secretImportOrm,
find,
+ findByFolderIds,
findLastImportPosition,
updateAllPosition
};
diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts
index 1beae9be6..2d59284d4 100644
--- a/backend/src/services/secret-import/secret-import-service.ts
+++ b/backend/src/services/secret-import/secret-import-service.ts
@@ -7,6 +7,7 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretDALFactory } from "../secret/secret-dal";
+import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "./secret-import-dal";
import { fnSecretsFromImports } from "./secret-import-fns";
@@ -25,6 +26,7 @@ type TSecretImportServiceFactoryDep = {
projectDAL: Pick;
projectEnvDAL: TProjectEnvDALFactory;
permissionService: Pick;
+ secretQueueService: Pick;
};
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
@@ -37,7 +39,8 @@ export const secretImportServiceFactory = ({
permissionService,
folderDAL,
projectDAL,
- secretDAL
+ secretDAL,
+ secretQueueService
}: TSecretImportServiceFactoryDep) => {
const createImport = async ({
environment,
@@ -45,10 +48,17 @@ export const secretImportServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
projectId,
path
}: TCreateSecretImportDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
// check if user has permission to import into destination path
ForbiddenError.from(permission).throwUnlessCan(
@@ -70,10 +80,19 @@ export const secretImportServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
- // TODO(akhilmhdh-pg): updated permission check add here
const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]);
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
+ const sourceFolder = await folderDAL.findBySecretPath(projectId, data.environment, data.path);
+ if (sourceFolder) {
+ const existingImport = await secretImportDAL.findOne({
+ folderId: sourceFolder.id,
+ importEnv: folder.environment.id,
+ importPath: path
+ });
+ if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
+ }
+
const secImport = await secretImportDAL.transaction(async (tx) => {
const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx);
return secretImportDAL.create(
@@ -87,6 +106,12 @@ export const secretImportServiceFactory = ({
);
});
+ await secretQueueService.syncSecrets({
+ secretPath: secImport.importPath,
+ projectId,
+ environment: importEnv.slug
+ });
+
return { ...secImport, importEnv };
};
@@ -97,10 +122,17 @@ export const secretImportServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
data,
id
}: TUpdateSecretImportDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -117,6 +149,20 @@ export const secretImportServiceFactory = ({
: await projectEnvDAL.findById(secImpDoc.importEnv);
if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
+ const sourceFolder = await folderDAL.findBySecretPath(
+ projectId,
+ importedEnv.slug,
+ data.path || secImpDoc.importPath
+ );
+ if (sourceFolder) {
+ const existingImport = await secretImportDAL.findOne({
+ folderId: sourceFolder.id,
+ importEnv: folder.environment.id,
+ importPath: path
+ });
+ if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
+ }
+
const updatedSecImport = await secretImportDAL.transaction(async (tx) => {
const secImp = await secretImportDAL.findOne({ folderId: folder.id, id });
if (!secImp) throw ERR_SEC_IMP_NOT_FOUND;
@@ -144,9 +190,16 @@ export const secretImportServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
id
}: TDeleteSecretImportDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -164,11 +217,32 @@ export const secretImportServiceFactory = ({
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
return { ...doc, importEnv };
});
+
+ await secretQueueService.syncSecrets({
+ secretPath: path,
+ projectId,
+ environment
+ });
+
return secImport;
};
- const getImports = async ({ path, environment, projectId, actor, actorId, actorOrgId }: TGetSecretImportsDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getImports = async ({
+ path,
+ environment,
+ projectId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TGetSecretImportsDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -186,10 +260,17 @@ export const secretImportServiceFactory = ({
environment,
projectId,
actor,
+ actorAuthMethod,
actorId,
actorOrgId
}: TGetSecretsFromImportDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts
index 1007ec4c3..ed8f5fec7 100644
--- a/backend/src/services/secret-tag/secret-tag-service.ts
+++ b/backend/src/services/secret-tag/secret-tag-service.ts
@@ -15,8 +15,23 @@ type TSecretTagServiceFactoryDep = {
export type TSecretTagServiceFactory = ReturnType;
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
- const createTag = async ({ name, slug, actor, color, actorId, actorOrgId, projectId }: TCreateTagDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const createTag = async ({
+ name,
+ slug,
+ actor,
+ color,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TCreateTagDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
const existingTag = await secretTagDAL.findOne({ slug, projectId });
@@ -32,19 +47,31 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return newTag;
};
- const deleteTag = async ({ actorId, actor, actorOrgId, id }: TDeleteTagDTO) => {
+ const deleteTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteTagDTO) => {
const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, tag.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ tag.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
const deletedTag = await secretTagDAL.deleteById(tag.id);
return deletedTag;
};
- const getProjectTags = async ({ actor, actorId, actorOrgId, projectId }: TListProjectTagsDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
const tags = await secretTagDAL.find({ projectId }, { sort: [["createdAt", "asc"]] });
diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts
index 11cd522ca..8a5970b83 100644
--- a/backend/src/services/secret/secret-dal.ts
+++ b/backend/src/services/secret/secret-dal.ts
@@ -150,6 +150,71 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
+ const getSecretTags = async (secretId: string, tx?: Knex) => {
+ try {
+ const tags = await (tx || db)(TableName.JnSecretTag)
+ .join(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
+ .where({ [`${TableName.Secret}Id` as const]: secretId })
+ .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
+ .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
+ .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
+ .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
+
+ return tags.map((el) => ({
+ id: el.tagId,
+ color: el.tagColor,
+ slug: el.tagSlug,
+ name: el.tagName
+ }));
+ } catch (error) {
+ throw new DatabaseError({ error, name: "get secret tags" });
+ }
+ };
+
+ const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
+ try {
+ // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
+ if (userId && !uuidValidate(userId)) {
+ // eslint-disable-next-line no-param-reassign
+ userId = undefined;
+ }
+
+ const secs = await (tx || db)(TableName.Secret)
+ .whereIn("folderId", folderIds)
+ .where((bd) => {
+ void bd.whereNull("userId").orWhere({ userId: userId || null });
+ })
+ .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
+ .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
+ .select(selectAllTableCols(TableName.Secret))
+ .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
+ .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
+ .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
+ .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
+ .orderBy("id", "asc");
+ const data = sqlNestRelationships({
+ data: secs,
+ key: "id",
+ parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
+ childrenMapper: [
+ {
+ key: "tagId",
+ label: "tags" as const,
+ mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
+ id,
+ color,
+ slug,
+ name
+ })
+ }
+ ]
+ });
+ return data;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "get all secret" });
+ }
+ };
+
const findByBlindIndexes = async (
folderId: string,
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
@@ -184,7 +249,9 @@ export const secretDALFactory = (db: TDbClient) => {
bulkUpdate,
deleteMany,
bulkUpdateNoVersionIncrement,
+ getSecretTags,
findByFolderId,
+ findByFolderIds,
findByBlindIndexes
};
};
diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts
index 212bb01f0..2bfda2cbe 100644
--- a/backend/src/services/secret/secret-fns.ts
+++ b/backend/src/services/secret/secret-fns.ts
@@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */
+import { subject } from "@casl/ability";
import path from "path";
import {
@@ -7,8 +8,11 @@ import {
SecretType,
TableName,
TSecretBlindIndexes,
+ TSecretFolders,
TSecrets
} from "@app/db/schemas";
+import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
+import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import {
buildSecretBlindIndexFromName,
@@ -17,8 +21,11 @@ import {
} from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn";
+import { logger } from "@app/lib/logger";
+import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
+import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal";
import {
@@ -45,6 +52,141 @@ export const generateSecretBlindIndexBySalt = async (secretName: string, secretB
return secretBlindIndex;
};
+type TRecursivelyFetchSecretsFromFoldersArg = {
+ permissionService: Pick;
+ folderDAL: Pick;
+ projectEnvDAL: Pick;
+};
+
+type TGetPathsDTO = {
+ projectId: string;
+ environment: string;
+ currentPath: string;
+
+ auth: {
+ actor: ActorType;
+ actorId: string;
+ actorAuthMethod: ActorAuthMethod;
+ actorOrgId: string | undefined;
+ };
+};
+
+// Introduce a new interface for mapping parent IDs to their children
+interface FolderMap {
+ [parentId: string]: TSecretFolders[];
+}
+const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
+ const map: FolderMap = {};
+ map.null = []; // Initialize mapping for root directory
+
+ folders.forEach((folder) => {
+ const parentId = folder.parentId || "null";
+ if (!map[parentId]) {
+ map[parentId] = [];
+ }
+ map[parentId].push(folder);
+ });
+
+ return map;
+};
+
+const generatePaths = (
+ map: FolderMap,
+ parentId: string = "null",
+ basePath: string = "",
+ currentDepth: number = 0
+): { path: string; folderId: string }[] => {
+ const children = map[parentId || "null"] || [];
+ let paths: { path: string; folderId: string }[] = [];
+
+ children.forEach((child) => {
+ // Determine if this is the root folder of the environment. If no parentId is present and the name is root, it's the root folder
+ const isRootFolder = child.name === "root" && !child.parentId;
+
+ // Form the current path based on the base path and the current child
+ // eslint-disable-next-line no-nested-ternary
+ const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`;
+
+ // Add the current path
+ paths.push({
+ path: currPath,
+ folderId: child.id
+ });
+
+ // We make sure that the recursion depth doesn't exceed 20.
+ // We do this to create "circuit break", basically to ensure that we can't encounter any potential memory leaks.
+ if (currentDepth >= 20) {
+ logger.info(`generatePaths: Recursion depth exceeded 20, breaking out of recursion [map=${JSON.stringify(map)}]`);
+ return;
+ }
+ // Recursively generate paths for children, passing down the formatted path
+ const childPaths = generatePaths(map, child.id, currPath, currentDepth + 1);
+ paths = paths.concat(
+ childPaths.map((p) => ({
+ path: p.path,
+ folderId: p.folderId
+ }))
+ );
+ });
+
+ return paths;
+};
+
+export const recursivelyGetSecretPaths = ({
+ folderDAL,
+ projectEnvDAL,
+ permissionService
+}: TRecursivelyFetchSecretsFromFoldersArg) => {
+ const getPaths = async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => {
+ const env = await projectEnvDAL.findOne({
+ projectId,
+ slug: environment
+ });
+
+ if (!env) {
+ throw new Error(`'${environment}' environment not found in project with ID ${projectId}`);
+ }
+
+ // Fetch all folders in env once with a single query
+ const folders = await folderDAL.find({
+ envId: env.id
+ });
+
+ // Build the folder hierarchy map
+ const folderMap = buildHierarchy(folders);
+
+ // Generate the paths paths and normalize the root path to /
+ const paths = generatePaths(folderMap).map((p) => ({
+ path: p.path === "/" ? p.path : p.path.substring(1),
+ folderId: p.folderId
+ }));
+
+ const { permission } = await permissionService.getProjectPermission(
+ auth.actor,
+ auth.actorId,
+ projectId,
+ auth.actorAuthMethod,
+ auth.actorOrgId
+ );
+
+ // Filter out paths that the user does not have permission to access, and paths that are not in the current path
+ const allowedPaths = paths.filter(
+ (folder) =>
+ permission.can(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, {
+ environment,
+ secretPath: folder.path
+ })
+ ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
+ );
+
+ return allowedPaths;
+ };
+
+ return getPaths;
+};
+
type TInterpolateSecretArg = {
projectId: string;
secretEncKey: string;
@@ -202,9 +344,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
);
// eslint-disable-next-line
- secrets[key].value = secrets[key].skipMultilineEncoding
- ? expandedVal
- : formatMultiValueEnv(expandedVal);
+ secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
}
return secrets;
@@ -212,7 +352,10 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
return expandSecrets;
};
-export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environment: string }, key: string) => {
+export const decryptSecretRaw = (
+ secret: TSecrets & { workspace: string; environment: string; secretPath?: string },
+ key: string
+) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
@@ -240,6 +383,7 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ
return {
secretKey,
+ secretPath: secret.secretPath,
workspace: secret.workspace,
environment: secret.environment,
secretValue,
diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts
index 2e5ea7f93..1fc6b1109 100644
--- a/backend/src/services/secret/secret-queue.ts
+++ b/backend/src/services/secret/secret-queue.ts
@@ -3,7 +3,7 @@ import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { BadRequestError } from "@app/lib/errors";
-import { isSamePath } from "@app/lib/fn";
+import { groupBy, isSamePath, unique } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -23,7 +23,6 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
-import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns";
@@ -32,7 +31,6 @@ import { interpolateSecrets } from "./secret-fns";
import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types";
export type TSecretQueueFactory = ReturnType;
-
type TSecretQueueFactoryDep = {
queueService: TQueueServiceFactory;
integrationDAL: Pick;
@@ -60,6 +58,8 @@ export type TGetSecrets = {
environment: string;
};
+const MAX_SYNC_SECRET_DEPTH = 5;
+
export const secretQueueFactory = ({
queueService,
integrationDAL,
@@ -117,7 +117,10 @@ export const secretQueueFactory = ({
});
};
- const syncSecrets = async (dto: TGetSecrets) => {
+ const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => {
+ logger.info(
+ `syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]`
+ );
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
removeOnFail: { count: 5 },
@@ -227,60 +230,42 @@ export const secretQueueFactory = ({
}
};
- const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => {
+ type Content = Record;
+
+ /**
+ * Return the secrets in a given [folderId] including secrets from
+ * nested imported folders recursively.
+ */
+ const getIntegrationSecrets = async (dto: {
+ projectId: string;
+ environment: string;
+ folderId: string;
+ key: string;
+ depth: number;
+ }) => {
+ let content: Content = {};
+ if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
+ logger.info(
+ `getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
+ );
+ return content;
+ }
+
+ // process secrets in current folder
const secrets = await secretDAL.findByFolderId(dto.folderId);
- if (!secrets.length) return {};
-
- // get imported secrets
- const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
- const importedSecrets = await fnSecretsFromImports({
- allowedImports: secretImport,
- secretDAL,
- folderDAL
- });
- const content: Record = {};
-
- importedSecrets.forEach(({ secrets: secs }) => {
- secs.forEach((secret) => {
- 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
- });
- content[secretKey] = { value: secretValue };
- content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
-
- if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
- const commentValue = decryptSymmetric128BitHexKeyUTF8({
- ciphertext: secret.secretCommentCiphertext,
- iv: secret.secretCommentIV,
- tag: secret.secretCommentTag,
- key
- });
- content[secretKey].comment = commentValue;
- }
- });
- });
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
- key
+ key: dto.key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
- key
+ key: dto.key
});
content[secretKey] = { value: secretValue };
@@ -290,38 +275,111 @@ export const secretQueueFactory = ({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
- key
+ key: dto.key
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
});
+
const expandSecrets = interpolateSecrets({
projectId: dto.projectId,
- secretEncKey: key,
+ secretEncKey: dto.key,
folderDAL,
secretDAL
});
+
await expandSecrets(content);
+
+ // check if current folder has any imports from other folders
+ const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
+
+ // if no imports then return secrets in the current folder
+ if (!secretImport) return content;
+
+ const importedFolders = await folderDAL.findByManySecretPath(
+ secretImport.map(({ importEnv, importPath }) => ({
+ envId: importEnv.id,
+ secretPath: importPath
+ }))
+ );
+
+ for await (const folder of importedFolders) {
+ if (folder) {
+ // get secrets contained in each imported folder by recursively calling
+ // this function against the imported folder
+ const importedSecrets = await getIntegrationSecrets({
+ environment: dto.environment,
+ projectId: dto.projectId,
+ folderId: folder.id,
+ key: dto.key,
+ depth: dto.depth + 1
+ });
+
+ // add the imported secrets to the current folder secrets
+ content = { ...content, ...importedSecrets };
+ }
+ }
+
return content;
};
queueService.start(QueueName.IntegrationSync, async (job) => {
- const { environment, projectId, secretPath } = job.data;
+ const { environment, projectId, secretPath, depth = 1 } = job.data;
+
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) {
- logger.error("Secret path not found");
+ logger.error(new Error("Secret path not found"));
return;
}
- const integrations = await integrationDAL.findByProjectIdV2(projectId, environment);
+ // start syncing all linked imports also
+ if (depth < MAX_SYNC_SECRET_DEPTH) {
+ // find all imports made with the given environment and secret path
+ const linkSourceDto = {
+ projectId,
+ importEnv: folder.environment.id,
+ importPath: secretPath
+ };
+ const imports = await secretImportDAL.find(linkSourceDto);
+
+ if (imports.length) {
+ // keep calling sync secret for all the imports made
+ const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
+ const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
+ const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id);
+ await Promise.all(
+ imports
+ .filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
+ .map(({ folderId }) => {
+ const syncDto = {
+ depth: depth + 1,
+ projectId,
+ secretPath: foldersGroupedById[folderId][0].path,
+ environment: foldersGroupedById[folderId][0].environmentSlug
+ };
+ logger.info(
+ `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
+ );
+ return syncSecrets(syncDto);
+ })
+ );
+ }
+ } else {
+ logger.info(`getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}]`);
+ }
+
+ const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
const toBeSyncedIntegrations = integrations.filter(
+ // note: sync only the integrations sourced from secretPath
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
);
if (!integrations.length) return;
- logger.info("Secret integration sync started", job.data, job.id);
+ logger.info(
+ `getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
+ );
for (const integration of toBeSyncedIntegrations) {
const integrationAuth = {
...integration.integrationAuth,
@@ -332,7 +390,13 @@ export const secretQueueFactory = ({
const botKey = await projectBotService.getBotKey(projectId);
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey);
- const secrets = await getIntegrationSecrets({ environment, projectId, secretPath, folderId: folder.id }, botKey);
+ const secrets = await getIntegrationSecrets({
+ environment,
+ projectId,
+ folderId: folder.id,
+ key: botKey,
+ depth: 1
+ });
const suffixedSecrets: typeof secrets = {};
const metadata = integration.metadata as Record;
if (metadata) {
@@ -360,7 +424,7 @@ export const secretQueueFactory = ({
});
}
- logger.info("Secret integration sync ended", job.id);
+ logger.info("Secret integration sync ended: %s", job.id);
});
queueService.start(QueueName.SecretReminder, async ({ data }) => {
@@ -401,7 +465,7 @@ export const secretQueueFactory = ({
});
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
- logger.error("Failed to sync integration", job?.data, err);
+ logger.error(err, "Failed to sync integration %s", job?.id);
});
queueService.start(QueueName.SecretWebhook, async (job) => {
@@ -409,7 +473,8 @@ export const secretQueueFactory = ({
});
return {
- syncSecrets,
+ // depth is internal only field thus no need to make it available outside
+ syncSecrets: (dto: TGetSecrets) => syncSecrets(dto),
syncIntegrations,
addSecretReminder,
removeSecretReminder,
diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts
index 51136e40f..3b504fbf4 100644
--- a/backend/src/services/secret/secret-service.ts
+++ b/backend/src/services/secret/secret-service.ts
@@ -1,3 +1,5 @@
+/* eslint-disable no-unreachable-loop */
+/* eslint-disable no-await-in-loop */
import { ForbiddenError, subject } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
@@ -13,15 +15,23 @@ import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
+import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretDALFactory } from "./secret-dal";
-import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
+import {
+ decryptSecretRaw,
+ fnSecretBlindIndexCheck,
+ fnSecretBulkInsert,
+ fnSecretBulkUpdate,
+ recursivelyGetSecretPaths
+} from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue";
import {
+ TAttachSecretTagsDTO,
TCreateBulkSecretDTO,
TCreateSecretDTO,
TCreateSecretRawDTO,
@@ -46,20 +56,25 @@ type TSecretServiceFactoryDep = {
secretDAL: TSecretDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
- folderDAL: Pick;
- projectDAL: Pick;
+ projectDAL: Pick;
+ projectEnvDAL: Pick;
+ folderDAL: Pick<
+ TSecretFolderDALFactory,
+ "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
+ >;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick;
snapshotService: Pick;
secretQueueService: Pick;
projectBotService: Pick;
- secretImportDAL: Pick;
+ secretImportDAL: Pick;
secretVersionTagDAL: Pick;
};
export type TSecretServiceFactory = ReturnType;
export const secretServiceFactory = ({
secretDAL,
+ projectEnvDAL,
secretTagDAL,
secretVersionDAL,
folderDAL,
@@ -145,10 +160,17 @@ export const secretServiceFactory = ({
actorId,
actorOrgId,
environment,
+ actorAuthMethod,
projectId,
...inputSecret
}: TCreateSecretDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -230,10 +252,17 @@ export const secretServiceFactory = ({
actorId,
actorOrgId,
environment,
+ actorAuthMethod,
projectId,
...inputSecret
}: TUpdateSecretDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -293,6 +322,7 @@ export const secretServiceFactory = ({
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const { secretName, ...el } = inputSecret;
+
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
@@ -341,11 +371,18 @@ export const secretServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
environment,
projectId,
...inputSecret
}: TDeleteSecretDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -401,21 +438,63 @@ export const secretServiceFactory = ({
projectId,
actor,
actorOrgId,
- includeImports
+ actorAuthMethod,
+ includeImports,
+ recursive
}: TGetSecretsDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
- ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Read,
- subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
);
- const folder = await folderDAL.findBySecretPath(projectId, environment, path);
- if (!folder) return { secrets: [], imports: [] };
- const folderId = folder.id;
+ let paths: { folderId: string; path: string }[] = [];
+
+ if (recursive) {
+ const getPaths = recursivelyGetSecretPaths({
+ permissionService,
+ folderDAL,
+ projectEnvDAL
+ });
+
+ const deepPaths = await getPaths({
+ projectId,
+ environment,
+ currentPath: path,
+ auth: {
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }
+ });
+
+ if (!deepPaths) return { secrets: [], imports: [] };
+
+ paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
+ } else {
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Read,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
+ );
+
+ const folder = await folderDAL.findBySecretPath(projectId, environment, path);
+ if (!folder) return { secrets: [], imports: [] };
+
+ paths = [{ folderId: folder.id, path }];
+ }
+
+ const groupedPaths = groupBy(paths, (p) => p.folderId);
+
+ const secrets = await secretDAL.findByFolderIds(
+ paths.map((p) => p.folderId),
+ actorId
+ );
- const secrets = await secretDAL.findByFolderId(folderId, actorId);
if (includeImports) {
- const secretImports = await secretImportDAL.find({ folderId });
+ const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
// if its service token allow full access over imported one
actor === ActorType.SERVICE
@@ -433,18 +512,33 @@ export const secretServiceFactory = ({
secretDAL,
folderDAL
});
+
return {
- secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })),
+ secrets: secrets.map((secret) => ({
+ ...secret,
+ workspace: projectId,
+ environment,
+ secretPath: groupedPaths[secret.folderId][0].path
+ })),
imports: importedSecrets
};
}
- return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) };
+
+ return {
+ secrets: secrets.map((secret) => ({
+ ...secret,
+ workspace: projectId,
+ environment,
+ secretPath: groupedPaths[secret.folderId][0].path
+ }))
+ };
};
const getSecretByName = async ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
projectId,
environment,
path,
@@ -453,7 +547,13 @@ export const secretServiceFactory = ({
version,
includeImports
}: TGetASecretDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -534,12 +634,19 @@ export const secretServiceFactory = ({
path,
actor,
actorId,
+ actorAuthMethod,
actorOrgId,
environment,
projectId,
secrets: inputSecrets
}: TCreateBulkSecretDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
@@ -597,13 +704,20 @@ export const secretServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
environment,
projectId,
secrets: inputSecrets
}: TUpdateBulkSecretDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -678,11 +792,18 @@ export const secretServiceFactory = ({
projectId,
actor,
actorId,
+ actorAuthMethod,
actorOrgId
}: TDeleteBulkSecretDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
+ ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
@@ -728,8 +849,10 @@ export const secretServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
environment,
- includeImports
+ includeImports,
+ recursive
}: TGetSecretsRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
@@ -740,8 +863,10 @@ export const secretServiceFactory = ({
environment,
actor,
actorOrgId,
+ actorAuthMethod,
path,
- includeImports
+ includeImports,
+ recursive
});
return {
@@ -749,7 +874,10 @@ export const secretServiceFactory = ({
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
...el,
secrets: importedSecrets.map((sec) =>
- decryptSecretRaw({ ...sec, environment: el.environment, workspace: projectId }, botKey)
+ decryptSecretRaw(
+ { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
+ botKey
+ )
)
}))
};
@@ -763,6 +891,7 @@ export const secretServiceFactory = ({
projectId,
actorId,
actorOrgId,
+ actorAuthMethod,
secretName,
includeImports,
version
@@ -773,6 +902,7 @@ export const secretServiceFactory = ({
const secret = await getSecretByName({
actorId,
projectId,
+ actorAuthMethod,
environment,
actor,
actorOrgId,
@@ -792,6 +922,7 @@ export const secretServiceFactory = ({
environment,
actor,
actorOrgId,
+ actorAuthMethod,
type,
secretPath,
secretValue,
@@ -813,6 +944,7 @@ export const secretServiceFactory = ({
path: secretPath,
actor,
actorId,
+ actorAuthMethod,
actorOrgId,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
@@ -839,6 +971,7 @@ export const secretServiceFactory = ({
environment,
actor,
actorOrgId,
+ actorAuthMethod,
type,
secretPath,
secretValue,
@@ -858,6 +991,7 @@ export const secretServiceFactory = ({
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
@@ -877,6 +1011,7 @@ export const secretServiceFactory = ({
environment,
actor,
actorOrgId,
+ actorAuthMethod,
type,
secretPath
}: TDeleteSecretRawDTO) => {
@@ -891,7 +1026,8 @@ export const secretServiceFactory = ({
path: secretPath,
actor,
actorId,
- actorOrgId
+ actorOrgId,
+ actorAuthMethod
});
await snapshotService.performSnapshot(secret.folderId);
@@ -904,6 +1040,7 @@ export const secretServiceFactory = ({
actorId,
actor,
actorOrgId,
+ actorAuthMethod,
limit = 20,
offset = 0,
secretId
@@ -914,14 +1051,222 @@ export const secretServiceFactory = ({
const folder = await folderDAL.findById(secret.folderId);
if (!folder) throw new BadRequestError({ message: "Failed to find secret" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ folder.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
return secretVersions;
};
+ const attachTags = async ({
+ secretName,
+ tagSlugs,
+ path: secretPath,
+ environment,
+ type,
+ projectSlug,
+ actor,
+ actorAuthMethod,
+ actorOrgId,
+ actorId
+ }: TAttachSecretTagsDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Edit,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath })
+ );
+
+ await projectDAL.checkProjectUpgradeStatus(project.id);
+
+ const secret = await getSecretByName({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ projectId: project.id,
+ environment,
+ path: secretPath,
+ secretName,
+ type
+ });
+
+ if (!secret) {
+ throw new BadRequestError({ message: "Secret not found" });
+ }
+ const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
+
+ if (!folder) {
+ throw new BadRequestError({ message: "Folder not found" });
+ }
+
+ const tags = await secretTagDAL.find({
+ projectId: project.id,
+ $in: {
+ slug: tagSlugs
+ }
+ });
+
+ if (tags.length !== tagSlugs.length) {
+ throw new BadRequestError({ message: "One or more tags not found." });
+ }
+
+ const existingSecretTags = await secretDAL.getSecretTags(secret.id);
+
+ if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) {
+ throw new BadRequestError({ message: "One or more tags already exist on the secret" });
+ }
+
+ const combinedTags = new Set([...existingSecretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
+
+ const updatedSecret = await secretDAL.transaction(async (tx) =>
+ fnSecretBulkUpdate({
+ folderId: folder.id,
+ projectId: project.id,
+ inputSecrets: [
+ {
+ filter: { id: secret.id },
+ data: {
+ tags: Array.from(combinedTags)
+ }
+ }
+ ],
+ secretDAL,
+ secretVersionDAL,
+ secretTagDAL,
+ secretVersionTagDAL,
+ tx
+ })
+ );
+
+ await snapshotService.performSnapshot(folder.id);
+ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
+
+ return {
+ ...updatedSecret[0],
+ tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
+ };
+ };
+
+ const detachTags = async ({
+ secretName,
+ tagSlugs,
+ path: secretPath,
+ environment,
+ type,
+ projectSlug,
+ actor,
+ actorAuthMethod,
+ actorOrgId,
+ actorId
+ }: TAttachSecretTagsDTO) => {
+ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
+
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ project.id,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ ProjectPermissionActions.Edit,
+ subject(ProjectPermissionSub.Secrets, { environment, secretPath })
+ );
+
+ await projectDAL.checkProjectUpgradeStatus(project.id);
+
+ const secret = await getSecretByName({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ projectId: project.id,
+ environment,
+ path: secretPath,
+ secretName,
+ type
+ });
+
+ if (!secret) {
+ throw new BadRequestError({ message: "Secret not found" });
+ }
+ const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
+
+ if (!folder) {
+ throw new BadRequestError({ message: "Folder not found" });
+ }
+
+ const tags = await secretTagDAL.find({
+ projectId: project.id,
+ $in: {
+ slug: tagSlugs
+ }
+ });
+
+ if (tags.length !== tagSlugs.length) {
+ throw new BadRequestError({ message: "One or more tags not found." });
+ }
+
+ const existingSecretTags = await secretDAL.getSecretTags(secret.id);
+
+ // Make sure all the tags exist on the secret
+ const tagIdsToRemove = tags.map((tag) => tag.id);
+ const secretTagIds = existingSecretTags.map((tag) => tag.id);
+
+ if (!tagIdsToRemove.every((el) => secretTagIds.includes(el))) {
+ throw new BadRequestError({ message: "One or more tags not found on the secret" });
+ }
+
+ const newTags = existingSecretTags.filter((tag) => !tagIdsToRemove.includes(tag.id));
+
+ const updatedSecret = await secretDAL.transaction(async (tx) =>
+ fnSecretBulkUpdate({
+ folderId: folder.id,
+ projectId: project.id,
+ inputSecrets: [
+ {
+ filter: { id: secret.id },
+ data: {
+ tags: newTags.map((tag) => tag.id)
+ }
+ }
+ ],
+ secretDAL,
+ secretVersionDAL,
+ secretTagDAL,
+ secretVersionTagDAL,
+ tx
+ })
+ );
+
+ await snapshotService.performSnapshot(folder.id);
+ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
+
+ return {
+ ...updatedSecret[0],
+ tags: newTags
+ };
+ };
+
return {
+ attachTags,
+ detachTags,
createSecret,
deleteSecret,
updateSecret,
diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts
index 7ad4d65d7..22347de4e 100644
--- a/backend/src/services/secret/secret-types.ts
+++ b/backend/src/services/secret/secret-types.ts
@@ -74,6 +74,7 @@ export type TGetSecretsDTO = {
path: string;
environment: string;
includeImports?: boolean;
+ recursive?: boolean;
} & TProjectPermission;
export type TGetASecretDTO = {
@@ -140,6 +141,7 @@ export type TGetSecretsRawDTO = {
path: string;
environment: string;
includeImports?: boolean;
+ recursive?: boolean;
} & TProjectPermission;
export type TGetASecretRawDTO = {
@@ -206,6 +208,15 @@ export type TFnSecretBulkUpdate = {
tx?: Knex;
};
+export type TAttachSecretTagsDTO = {
+ projectSlug: string;
+ secretName: string;
+ tagSlugs: string[];
+ environment: string;
+ path: string;
+ type: SecretType;
+} & Omit;
+
export type TFnSecretBulkDelete = {
folderId: string;
projectId: string;
diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts
index cce0d3780..e434bd91f 100644
--- a/backend/src/services/service-token/service-token-service.ts
+++ b/backend/src/services/service-token/service-token-service.ts
@@ -9,6 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorType } from "../auth/auth-type";
+import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TServiceTokenDALFactory } from "./service-token-dal";
@@ -24,6 +25,7 @@ type TServiceTokenServiceFactoryDep = {
userDAL: TUserDALFactory;
permissionService: Pick;
projectEnvDAL: Pick;
+ projectDAL: Pick;
};
export type TServiceTokenServiceFactory = ReturnType;
@@ -32,7 +34,8 @@ export const serviceTokenServiceFactory = ({
serviceTokenDAL,
userDAL,
permissionService,
- projectEnvDAL
+ projectEnvDAL,
+ projectDAL
}: TServiceTokenServiceFactoryDep) => {
const createServiceToken = async ({
iv,
@@ -40,6 +43,7 @@ export const serviceTokenServiceFactory = ({
name,
actor,
actorOrgId,
+ actorAuthMethod,
scopes,
actorId,
projectId,
@@ -47,7 +51,13 @@ export const serviceTokenServiceFactory = ({
permissions,
encryptedKey
}: TCreateServiceTokenDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
scopes.forEach(({ environment, secretPath }) => {
@@ -91,7 +101,7 @@ export const serviceTokenServiceFactory = ({
return { token, serviceToken };
};
- const deleteServiceToken = async ({ actorId, actor, actorOrgId, id }: TDeleteServiceTokenDTO) => {
+ const deleteServiceToken = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteServiceTokenDTO) => {
const serviceToken = await serviceTokenDAL.findById(id);
if (!serviceToken) throw new BadRequestError({ message: "Token not found" });
@@ -99,6 +109,7 @@ export const serviceTokenServiceFactory = ({
actor,
actorId,
serviceToken.projectId,
+ actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
@@ -119,8 +130,20 @@ export const serviceTokenServiceFactory = ({
return { serviceToken, user: serviceTokenUser };
};
- const getProjectServiceTokens = async ({ actorId, actor, actorOrgId, projectId }: TProjectServiceTokensDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const getProjectServiceTokens = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ projectId
+ }: TProjectServiceTokensDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
const tokens = await serviceTokenDAL.find({ projectId }, { sort: [["createdAt", "desc"]] });
@@ -130,7 +153,11 @@ export const serviceTokenServiceFactory = ({
const fnValidateServiceToken = async (token: string) => {
const [, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>token.split(".", 3);
const serviceToken = await serviceTokenDAL.findById(TOKEN_IDENTIFIER);
+
if (!serviceToken) throw new UnauthorizedError();
+ const project = await projectDAL.findById(serviceToken.projectId);
+
+ if (!project) throw new UnauthorizedError({ message: "Service token project not found" });
if (serviceToken.expiresAt && new Date(serviceToken.expiresAt) < new Date()) {
await serviceTokenDAL.deleteById(serviceToken.id);
@@ -142,7 +169,8 @@ export const serviceTokenServiceFactory = ({
const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, {
lastUsed: new Date()
});
- return { ...serviceToken, lastUsed: updatedToken.lastUsed };
+
+ return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId };
};
return {
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index d76ad3ec3..07fc2e991 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -136,6 +136,7 @@ export const superAdminServiceFactory = ({
await updateServerCfg({ initialized: true });
const token = await authService.generateUserTokens({
user: userInfo.user,
+ authMethod: AuthMethod.EMAIL,
ip,
userAgent,
organizationId: undefined
diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts
index c3919cc50..4a05ad219 100644
--- a/backend/src/services/webhook/webhook-service.ts
+++ b/backend/src/services/webhook/webhook-service.ts
@@ -31,13 +31,20 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
actor,
actorId,
actorOrgId,
+ actorAuthMethod,
projectId,
webhookUrl,
environment,
secretPath,
webhookSecretKey
}: TCreateWebhookDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Env not found" });
@@ -73,33 +80,51 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
return { ...webhook, projectId, environment: env };
};
- const updateWebhook = async ({ actorId, actor, actorOrgId, id, isDisabled }: TUpdateWebhookDTO) => {
+ const updateWebhook = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, isDisabled }: TUpdateWebhookDTO) => {
const webhook = await webhookDAL.findById(id);
if (!webhook) throw new BadRequestError({ message: "Webhook not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, webhook.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ webhook.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
const updatedWebhook = await webhookDAL.updateById(id, { isDisabled });
return { ...webhook, ...updatedWebhook };
};
- const deleteWebhook = async ({ id, actor, actorId, actorOrgId }: TDeleteWebhookDTO) => {
+ const deleteWebhook = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteWebhookDTO) => {
const webhook = await webhookDAL.findById(id);
if (!webhook) throw new BadRequestError({ message: "Webhook not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, webhook.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ webhook.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
const deletedWebhook = await webhookDAL.deleteById(id);
return { ...webhook, ...deletedWebhook };
};
- const testWebhook = async ({ id, actor, actorId, actorOrgId }: TTestWebhookDTO) => {
+ const testWebhook = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TTestWebhookDTO) => {
const webhook = await webhookDAL.findById(id);
if (!webhook) throw new BadRequestError({ message: "Webhook not found" });
- const { permission } = await permissionService.getProjectPermission(actor, actorId, webhook.projectId, actorOrgId);
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ webhook.projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
let webhookError: string | undefined;
@@ -119,8 +144,22 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer
return { ...webhook, ...updatedWebhook };
};
- const listWebhooks = async ({ actorId, actor, actorOrgId, projectId, secretPath, environment }: TListWebhookDTO) => {
- const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
+ const listWebhooks = async ({
+ actorId,
+ actor,
+ actorOrgId,
+ actorAuthMethod,
+ projectId,
+ secretPath,
+ environment
+ }: TListWebhookDTO) => {
+ const { permission } = await permissionService.getProjectPermission(
+ actor,
+ actorId,
+ projectId,
+ actorAuthMethod,
+ actorOrgId
+ );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
return webhookDAL.findAllWebhooks(projectId, environment, secretPath);
diff --git a/backend/tsup.config.js b/backend/tsup.config.js
index 4e182b9d2..9e37870ba 100644
--- a/backend/tsup.config.js
+++ b/backend/tsup.config.js
@@ -23,7 +23,8 @@ export default defineConfig({
loader: {
".handlebars": "copy",
".md": "copy",
- ".txt": "copy"
+ ".txt": "copy",
+ ".pem": "copy"
},
external: ["../../../frontend/node_modules/next/dist/server/next-server.js"],
outDir: "dist",
diff --git a/cli/.gitignore b/cli/.gitignore
index dcc148f21..5fa3e39c5 100644
--- a/cli/.gitignore
+++ b/cli/.gitignore
@@ -1,2 +1,3 @@
.infisical.json
dist/
+agent-config.test.yaml
diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go
index 1b69b8f23..d45a42db4 100644
--- a/cli/packages/api/api.go
+++ b/cli/packages/api/api.go
@@ -164,6 +164,28 @@ func CallGetAllOrganizations(httpClient *resty.Client) (GetOrganizationsResponse
return orgResponse, nil
}
+func CallSelectOrganization(httpClient *resty.Client, request SelectOrganizationRequest) (SelectOrganizationResponse, error) {
+ var selectOrgResponse SelectOrganizationResponse
+
+ response, err := httpClient.
+ R().
+ SetBody(request).
+ SetResult(&selectOrgResponse).
+ SetHeader("User-Agent", USER_AGENT).
+ Post(fmt.Sprintf("%v/v3/auth/select-organization", config.INFISICAL_URL))
+
+ if err != nil {
+ return SelectOrganizationResponse{}, err
+ }
+
+ if response.IsError() {
+ return SelectOrganizationResponse{}, fmt.Errorf("CallSelectOrganization: Unsuccessful response: [response=%v]", response)
+ }
+
+ return selectOrgResponse, nil
+
+}
+
func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) {
var workSpacesResponse GetWorkSpacesResponse
response, err := httpClient.
@@ -255,6 +277,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
+ if request.Recursive {
+ httpRequest.SetQueryParam("recursive", "true")
+ }
+
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
@@ -384,14 +410,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
return nil
}
-func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
+func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
- Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
+ Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, secretName))
if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
@@ -513,3 +539,23 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
return getRawSecretsV3Response, nil
}
+
+func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDynamicSecretLeaseV1Request) (CreateDynamicSecretLeaseV1Response, error) {
+ var createDynamicSecretLeaseResponse CreateDynamicSecretLeaseV1Response
+ response, err := httpClient.
+ R().
+ SetResult(&createDynamicSecretLeaseResponse).
+ SetHeader("User-Agent", USER_AGENT).
+ SetBody(request).
+ Post(fmt.Sprintf("%v/v1/dynamic-secrets/leases", config.INFISICAL_URL))
+
+ if err != nil {
+ return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unable to complete api request [err=%w]", err)
+ }
+
+ if response.IsError() {
+ return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
+ }
+
+ return createDynamicSecretLeaseResponse, nil
+}
diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go
index 310976078..0a5bfee6d 100644
--- a/cli/packages/api/model.go
+++ b/cli/packages/api/model.go
@@ -135,6 +135,14 @@ type GetOrganizationsResponse struct {
} `json:"organizations"`
}
+type SelectOrganizationResponse struct {
+ Token string `json:"token"`
+}
+
+type SelectOrganizationRequest struct {
+ OrganizationId string `json:"organizationId"`
+}
+
type Secret struct {
SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"`
SecretKeyIV string `json:"secretKeyIV,omitempty"`
@@ -283,6 +291,7 @@ type GetEncryptedSecretsV3Request struct {
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
+ Recursive bool `json:"recursive"`
}
type GetFoldersV1Request struct {
@@ -393,7 +402,6 @@ type DeleteSecretV3Request struct {
}
type UpdateSecretByNameV3Request struct {
- SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
@@ -493,11 +501,34 @@ type UniversalAuthRefreshResponse struct {
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
}
+type CreateDynamicSecretLeaseV1Request struct {
+ Environment string `json:"environment"`
+ ProjectSlug string `json:"projectSlug"`
+ SecretPath string `json:"secretPath,omitempty"`
+ Slug string `json:"slug"`
+ TTL string `json:"ttl,omitempty"`
+}
+
+type CreateDynamicSecretLeaseV1Response struct {
+ Lease struct {
+ Id string `json:"id"`
+ ExpireAt time.Time `json:"expireAt"`
+ } `json:"lease"`
+ DynamicSecret struct {
+ Id string `json:"id"`
+ DefaultTTL string `json:"defaultTTL"`
+ MaxTTL string `json:"maxTTL"`
+ Type string `json:"type"`
+ } `json:"dynamicSecret"`
+ Data map[string]interface{} `json:"data"`
+}
+
type GetRawSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
+ Recursive bool `json:"recursive"`
}
type GetRawSecretsV3Response struct {
diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go
index db7c81225..03bf9af4d 100644
--- a/cli/packages/cmd/agent.go
+++ b/cli/packages/cmd/agent.go
@@ -14,6 +14,7 @@ import (
"os/signal"
"path"
"runtime"
+ "slices"
"strings"
"sync"
"syscall"
@@ -33,6 +34,9 @@ import (
const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"
+// duration to reduce from expiry of dynamic leases so that it gets triggered before expiry
+const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15
+
type Config struct {
Infisical InfisicalConfig `yaml:"infisical"`
Auth AuthConfig `yaml:"auth"`
@@ -84,6 +88,115 @@ type Template struct {
} `yaml:"config"`
}
+func newAgentTemplateChannels(templates []Template) map[string]chan bool {
+ // we keep each destination as an identifier for various channel
+ templateChannel := make(map[string]chan bool)
+ for _, template := range templates {
+ templateChannel[template.DestinationPath] = make(chan bool)
+ }
+ return templateChannel
+}
+
+type DynamicSecretLease struct {
+ LeaseID string
+ ExpireAt time.Time
+ Environment string
+ SecretPath string
+ Slug string
+ ProjectSlug string
+ Data map[string]interface{}
+ TemplateIDs []int
+}
+
+type DynamicSecretLeaseManager struct {
+ leases []DynamicSecretLease
+ mutex sync.Mutex
+}
+
+func (d *DynamicSecretLeaseManager) Prune() {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ d.leases = slices.DeleteFunc(d.leases, func(s DynamicSecretLease) bool {
+ return time.Now().After(s.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second))
+ })
+}
+
+func (d *DynamicSecretLeaseManager) Append(lease DynamicSecretLease) {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ index := slices.IndexFunc(d.leases, func(s DynamicSecretLease) bool {
+ if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug {
+ return true
+ }
+ return false
+ })
+
+ if index != -1 {
+ d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, lease.TemplateIDs...)
+ return
+ }
+ d.leases = append(d.leases, lease)
+}
+
+func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, secretPath, slug string, templateId int) {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ index := slices.IndexFunc(d.leases, func(lease DynamicSecretLease) bool {
+ if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
+ return true
+ }
+ return false
+ })
+
+ if index != -1 {
+ d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, templateId)
+ }
+}
+
+func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ for _, lease := range d.leases {
+ if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
+ return &lease
+ }
+ }
+
+ return nil
+}
+
+// for a given template find the first expiring lease
+// The bool indicates whether it contains valid expiry list
+func (d *DynamicSecretLeaseManager) GetFirstExpiringLeaseTime(templateId int) (time.Time, bool) {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ if len(d.leases) == 0 {
+ return time.Time{}, false
+ }
+
+ var firstExpiry time.Time
+ for i, el := range d.leases {
+ if i == 0 {
+ firstExpiry = el.ExpireAt
+ }
+ newLeaseTime := el.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second)
+ if newLeaseTime.Before(firstExpiry) {
+ firstExpiry = newLeaseTime
+ }
+ }
+ return firstExpiry, true
+}
+
+func NewDynamicSecretLeaseManager(sigChan chan os.Signal) *DynamicSecretLeaseManager {
+ manager := &DynamicSecretLeaseManager{}
+ return manager
+}
+
func ReadFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
}
@@ -219,7 +332,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
- res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false)
+ res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false, false)
if err != nil {
return nil, err
}
@@ -234,15 +347,49 @@ func secretTemplateFunction(accessToken string, existingEtag string, currentEtag
}
}
-func ProcessTemplate(templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) {
+func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *DynamicSecretLeaseManager, templateId int) func(...string) (map[string]interface{}, error) {
+ return func(args ...string) (map[string]interface{}, error) {
+ argLength := len(args)
+ if argLength != 4 && argLength != 5 {
+ return nil, fmt.Errorf("Invalid arguments found for dynamic-secret function. Check template %i", templateId)
+ }
+
+ projectSlug, envSlug, secretPath, slug, ttl := args[0], args[1], args[2], args[3], ""
+ if argLength == 5 {
+ ttl = args[4]
+ }
+ dynamicSecretData := dynamicSecretManager.GetLease(projectSlug, envSlug, secretPath, slug)
+ if dynamicSecretData != nil {
+ dynamicSecretManager.RegisterTemplate(projectSlug, envSlug, secretPath, slug, templateId)
+ return dynamicSecretData.Data, nil
+ }
+
+ res, err := util.CreateDynamicSecretLease(accessToken, projectSlug, envSlug, secretPath, slug, ttl)
+ if err != nil {
+ return nil, err
+ }
+
+ dynamicSecretManager.Append(DynamicSecretLease{LeaseID: res.Lease.Id, ExpireAt: res.Lease.ExpireAt, Environment: envSlug, SecretPath: secretPath, Slug: slug, ProjectSlug: projectSlug, Data: res.Data, TemplateIDs: []int{templateId}})
+ return res.Data, nil
+ }
+}
+
+func ProcessTemplate(templateId int, templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretManager *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
// custom template function to fetch secrets from Infisical
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag)
+ dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateId)
funcs := template.FuncMap{
- "secret": secretFunction,
+ "secret": secretFunction,
+ "dynamic_secret": dynamicSecretFunction,
+ "minus": func(a, b int) int {
+ return a - b
+ },
+ "add": func(a, b int) int {
+ return a + b
+ },
}
templateName := path.Base(templatePath)
-
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath)
if err != nil {
return nil, err
@@ -256,7 +403,7 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string,
return &buf, nil
}
-func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) {
+func ProcessBase64Template(templateId int, encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
// custom template function to fetch secrets from Infisical
decoded, err := base64.StdEncoding.DecodeString(encodedTemplate)
if err != nil {
@@ -266,8 +413,10 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
templateString := string(decoded)
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
+ dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
funcs := template.FuncMap{
- "secret": secretFunction,
+ "secret": secretFunction,
+ "dynamic_secret": dynamicSecretFunction,
}
templateName := "base64Template"
@@ -285,7 +434,7 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
return &buf, nil
}
-type TokenManager struct {
+type AgentManager struct {
accessToken string
accessTokenTTL time.Duration
accessTokenMaxTTL time.Duration
@@ -294,6 +443,7 @@ type TokenManager struct {
mutex sync.Mutex
filePaths []Sink // Store file paths if needed
templates []Template
+ dynamicSecretLeases *DynamicSecretLeaseManager
clientIdPath string
clientSecretPath string
newAccessTokenNotificationChan chan bool
@@ -302,8 +452,8 @@ type TokenManager struct {
exitAfterAuth bool
}
-func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *TokenManager {
- return &TokenManager{
+func NewAgentManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *AgentManager {
+ return &AgentManager{
filePaths: fileDeposits,
templates: templates,
clientIdPath: clientIdPath,
@@ -315,7 +465,7 @@ func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath str
}
-func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) {
+func (tm *AgentManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) {
tm.mutex.Lock()
defer tm.mutex.Unlock()
@@ -326,7 +476,7 @@ func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, acc
tm.newAccessTokenNotificationChan <- true
}
-func (tm *TokenManager) GetToken() string {
+func (tm *AgentManager) GetToken() string {
tm.mutex.Lock()
defer tm.mutex.Unlock()
@@ -334,8 +484,8 @@ func (tm *TokenManager) GetToken() string {
}
// Fetches a new access token using client credentials
-func (tm *TokenManager) FetchNewAccessToken() error {
- clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")
+func (tm *AgentManager) FetchNewAccessToken() error {
+ clientID := os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
if clientID == "" {
clientIDAsByte, err := ReadFile(tm.clientIdPath)
if err != nil {
@@ -365,7 +515,7 @@ func (tm *TokenManager) FetchNewAccessToken() error {
// save as cache in memory
tm.cachedClientSecret = clientSecret
- err, loginResponse := universalAuthLogin(clientID, clientSecret)
+ loginResponse, err := util.UniversalAuthLogin(clientID, clientSecret)
if err != nil {
return err
}
@@ -384,7 +534,7 @@ func (tm *TokenManager) FetchNewAccessToken() error {
}
// Refreshes the existing access token
-func (tm *TokenManager) RefreshAccessToken() error {
+func (tm *AgentManager) RefreshAccessToken() error {
httpClient := resty.New()
httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second).
@@ -405,7 +555,7 @@ func (tm *TokenManager) RefreshAccessToken() error {
return nil
}
-func (tm *TokenManager) ManageTokenLifecycle() {
+func (tm *AgentManager) ManageTokenLifecycle() {
for {
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
accessTokenRefreshedTime := tm.accessTokenRefreshedTime
@@ -473,7 +623,7 @@ func (tm *TokenManager) ManageTokenLifecycle() {
}
}
-func (tm *TokenManager) WriteTokenToFiles() {
+func (tm *AgentManager) WriteTokenToFiles() {
token := tm.GetToken()
for _, sinkFile := range tm.filePaths {
if sinkFile.Type == "file" {
@@ -490,7 +640,7 @@ func (tm *TokenManager) WriteTokenToFiles() {
}
}
-func (tm *TokenManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) {
+func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) {
if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil {
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
return
@@ -498,7 +648,7 @@ func (tm *TokenManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Templ
log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", template.SourcePath, template.DestinationPath)
}
-func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan chan os.Signal) {
+func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId int, sigChan chan os.Signal) {
pollingInterval := time.Duration(5 * time.Minute)
@@ -523,64 +673,64 @@ func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan ch
execCommand := secretTemplate.Config.Execute.Command
for {
- token := tm.GetToken()
+ select {
+ case <-sigChan:
+ return
+ default:
+ {
+ tm.dynamicSecretLeases.Prune()
+ token := tm.GetToken()
+ if token != "" {
+ var processedTemplate *bytes.Buffer
+ var err error
- if token != "" {
+ if secretTemplate.SourcePath != "" {
+ processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
+ } else {
+ processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
+ }
- var processedTemplate *bytes.Buffer
- var err error
+ if err != nil {
+ log.Error().Msgf("unable to process template because %v", err)
+ } else {
+ if (existingEtag != currentEtag) || firstRun {
- if secretTemplate.SourcePath != "" {
- processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag)
- } else {
- processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag)
- }
+ tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
+ existingEtag = currentEtag
- if err != nil {
- log.Error().Msgf("unable to process template because %v", err)
- } else {
- if (existingEtag != currentEtag) || firstRun {
+ if !firstRun && execCommand != "" {
+ log.Info().Msgf("executing command: %s", execCommand)
+ err := ExecuteCommandWithTimeout(execCommand, execTimeout)
- tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
- existingEtag = currentEtag
+ if err != nil {
+ log.Error().Msgf("unable to execute command because %v", err)
+ }
- if !firstRun && execCommand != "" {
- log.Info().Msgf("executing command: %s", execCommand)
- err := ExecuteCommandWithTimeout(execCommand, execTimeout)
-
- if err != nil {
- log.Error().Msgf("unable to execute command because %v", err)
+ }
+ if firstRun {
+ firstRun = false
+ }
}
+ }
+ // now the idea is we pick the next sleep time in which the one shorter out of
+ // - polling time
+ // - first lease that's gonna get expired in the template
+ firstLeaseExpiry, isValid := tm.dynamicSecretLeases.GetFirstExpiringLeaseTime(templateId)
+ var waitTime = pollingInterval
+ if isValid && firstLeaseExpiry.Sub(time.Now()) < pollingInterval {
+ waitTime = firstLeaseExpiry.Sub(time.Now())
}
- if firstRun {
- firstRun = false
- }
+ time.Sleep(waitTime)
+ } else {
+ // It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
+ time.Sleep(3 * time.Second)
}
}
- time.Sleep(pollingInterval)
- } else {
- // It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
- time.Sleep(3 * time.Second)
}
-
}
}
-func universalAuthLogin(clientId string, clientSecret string) (error, api.UniversalAuthLoginResponse) {
- httpClient := resty.New()
- httpClient.SetRetryCount(10000).
- SetRetryMaxWaitTime(20 * time.Second).
- SetRetryWaitTime(5 * time.Second)
-
- tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret})
- if err != nil {
- return err, api.UniversalAuthLoginResponse{}
- }
-
- return nil, tokenResponse
-}
-
// runCmd represents the run command
var agentCmd = &cobra.Command{
Example: `
@@ -645,13 +795,14 @@ var agentCmd = &cobra.Command{
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
filePaths := agentConfig.Sinks
- tm := NewTokenManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth)
+ tm := NewAgentManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth)
+ tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan)
go tm.ManageTokenLifecycle()
for i, template := range agentConfig.Templates {
log.Info().Msgf("template engine started for template %v...", i+1)
- go tm.MonitorSecretChanges(template, sigChan)
+ go tm.MonitorSecretChanges(template, i, sigChan)
}
for {
diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go
index 1a34219eb..84015cc02 100644
--- a/cli/packages/cmd/export.go
+++ b/cli/packages/cmd/export.go
@@ -7,6 +7,7 @@ import (
"encoding/csv"
"encoding/json"
"fmt"
+ "os"
"strings"
"github.com/Infisical/infisical-merge/packages/models"
@@ -44,6 +45,11 @@ var exportCmd = &cobra.Command{
util.HandleError(err)
}
+ includeImports, err := cmd.Flags().GetBool("include-imports")
+ if err != nil {
+ util.HandleError(err)
+ }
+
projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err)
@@ -54,13 +60,17 @@ var exportCmd = &cobra.Command{
util.HandleError(err)
}
+ templatePath, err := cmd.Flags().GetString("template")
+ if err != nil {
+ util.HandleError(err)
+ }
+
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
- infisicalToken, err := util.GetInfisicalServiceToken(cmd)
-
+ token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -75,7 +85,46 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
- secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId, SecretsPath: secretsPath}, "")
+ request := models.GetAllSecretsParameters{
+ Environment: environmentName,
+ TagSlugs: tagSlugs,
+ WorkspaceId: projectId,
+ SecretsPath: secretsPath,
+ IncludeImport: includeImports,
+ }
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ request.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ request.UniversalAuthAccessToken = token.Token
+ }
+
+ if templatePath != "" {
+ sigChan := make(chan os.Signal, 1)
+ dynamicSecretLeases := NewDynamicSecretLeaseManager(sigChan)
+ newEtag := ""
+
+ accessToken := ""
+ if token != nil {
+ accessToken = token.Token
+ } else {
+ log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
+ loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
+ if err != nil {
+ util.HandleError(err)
+ }
+ accessToken = loggedInUserDetails.UserCredentials.JTWToken
+ }
+
+ processedTemplate, err := ProcessTemplate(1, templatePath, nil, accessToken, "", &newEtag, dynamicSecretLeases)
+ if err != nil {
+ util.HandleError(err)
+ }
+ fmt.Print(processedTemplate.String())
+ return
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(request, "")
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
@@ -88,9 +137,16 @@ var exportCmd = &cobra.Command{
var output string
if shouldExpandSecrets {
- secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
- InfisicalToken: infisicalToken,
- }, "")
+
+ authParams := models.ExpandSecretsAuthentication{}
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ authParams.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ authParams.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets = util.ExpandSecrets(secrets, authParams, "")
}
secrets = util.FilterSecretsByTag(secrets, tagSlugs)
output, err = formatEnvs(secrets, format)
@@ -110,10 +166,12 @@ func init() {
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
+ exportCmd.Flags().Bool("include-imports", true, "Imported linked secrets")
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs")
exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from")
exportCmd.Flags().String("path", "/", "get secrets within a folder path")
+ exportCmd.Flags().String("template", "", "The path to the template file used to render secrets")
}
// Format according to the format flag
diff --git a/cli/packages/cmd/folder.go b/cli/packages/cmd/folder.go
index 290f32c38..b306960f4 100644
--- a/cli/packages/cmd/folder.go
+++ b/cli/packages/cmd/folder.go
@@ -36,18 +36,33 @@ var getCmd = &cobra.Command{
}
}
- infisicalToken, err := util.GetInfisicalServiceToken(cmd)
-
+ projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
+ token, err := util.GetInfisicalToken(cmd)
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
foldersPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
- folders, err := util.GetAllFolders(models.GetAllFoldersParameters{Environment: environmentName, InfisicalToken: infisicalToken, FoldersPath: foldersPath})
+ request := models.GetAllFoldersParameters{
+ Environment: environmentName,
+ WorkspaceId: projectId,
+ FoldersPath: foldersPath,
+ }
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ request.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ request.UniversalAuthAccessToken = token.Token
+ }
+
+ folders, err := util.GetAllFolders(request)
if err != nil {
util.HandleError(err, "Unable to get folders")
}
diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go
index e47eb447e..99d2ef502 100644
--- a/cli/packages/cmd/init.go
+++ b/cli/packages/cmd/init.go
@@ -74,6 +74,21 @@ var initCmd = &cobra.Command{
selectedOrganization := organizations[index]
+ tokenResponse, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID})
+
+ if err != nil {
+ util.HandleError(err, "Unable to select organization")
+ }
+
+ // set the config jwt token to the new token
+ userCreds.UserCredentials.JTWToken = tokenResponse.Token
+ err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials)
+ httpClient.SetAuthToken(tokenResponse.Token)
+
+ if err != nil {
+ util.HandleError(err, "Unable to store your user credentials")
+ }
+
workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient)
if err != nil {
util.HandleError(err, "Unable to pull projects that belong to you")
diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go
index 5cff7770f..bbb2c3a05 100644
--- a/cli/packages/cmd/login.go
+++ b/cli/packages/cmd/login.go
@@ -55,95 +55,157 @@ var loginCmd = &cobra.Command{
Short: "Login into your Infisical account",
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
- currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
- // if the key can't be found or there is an error getting current credentials from key ring, allow them to override
- if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
- log.Debug().Err(err)
- } else if err != nil {
+
+ loginMethod, err := cmd.Flags().GetString("method")
+ if err != nil {
+ util.HandleError(err)
+ }
+ plainOutput, err := cmd.Flags().GetBool("plain")
+ if err != nil {
util.HandleError(err)
}
- if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
- shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email)
- if err != nil {
+ if loginMethod != "user" && loginMethod != "universal-auth" {
+ util.PrintErrorMessageAndExit("Invalid login method. Please use either 'user' or 'universal-auth'")
+ }
+
+ if loginMethod == "user" {
+
+ currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
+ // if the key can't be found or there is an error getting current credentials from key ring, allow them to override
+ if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
+ log.Debug().Err(err)
+ } else if err != nil {
util.HandleError(err)
}
- if !shouldOverride {
- return
+ if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
+ shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email)
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ if !shouldOverride {
+ return
+ }
}
- }
- //override domain
- domainQuery := true
- if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL {
- overrideDomain, err := DomainOverridePrompt()
- if err != nil {
- util.HandleError(err)
+ //override domain
+ domainQuery := true
+ if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL {
+ overrideDomain, err := DomainOverridePrompt()
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ //if not override set INFISICAL_URL to exported var
+ //set domainQuery to false
+ if !overrideDomain {
+ domainQuery = false
+ config.INFISICAL_URL = config.INFISICAL_URL_MANUAL_OVERRIDE
+ }
+
}
- //if not override set INFISICAL_URL to exported var
- //set domainQuery to false
- if !overrideDomain {
- domainQuery = false
- config.INFISICAL_URL = config.INFISICAL_URL_MANUAL_OVERRIDE
+ //prompt user to select domain between Infisical cloud and self hosting
+ if domainQuery {
+ err = askForDomain()
+ if err != nil {
+ util.HandleError(err, "Unable to parse domain url")
+ }
}
+ var userCredentialsToBeStored models.UserCredentials
- }
-
- //prompt user to select domain between Infisical cloud and self hosting
- if domainQuery {
- err = askForDomain()
- if err != nil {
- util.HandleError(err, "Unable to parse domain url")
- }
- }
- var userCredentialsToBeStored models.UserCredentials
-
- interactiveLogin := false
- if cmd.Flags().Changed("interactive") {
- interactiveLogin = true
- cliDefaultLogin(&userCredentialsToBeStored)
- }
-
- //call browser login function
- if !interactiveLogin {
- fmt.Println("Logging in via browser... To login via interactive mode run [infisical login -i]")
- userCredentialsToBeStored, err = browserCliLogin()
- if err != nil {
- //default to cli login on error
+ interactiveLogin := false
+ if cmd.Flags().Changed("interactive") {
+ interactiveLogin = true
cliDefaultLogin(&userCredentialsToBeStored)
}
+
+ //call browser login function
+ if !interactiveLogin {
+ fmt.Println("Logging in via browser... To login via interactive mode run [infisical login -i]")
+ userCredentialsToBeStored, err = browserCliLogin()
+ if err != nil {
+ //default to cli login on error
+ cliDefaultLogin(&userCredentialsToBeStored)
+ }
+ }
+
+ err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored)
+ if err != nil {
+ log.Error().Msgf("Unable to store your credentials in system vault [%s]")
+ log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq")
+ log.Debug().Err(err)
+ //return here
+ util.HandleError(err)
+ }
+
+ err = util.WriteInitalConfig(&userCredentialsToBeStored)
+ if err != nil {
+ util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
+ }
+
+ // clear backed up secrets from prev account
+ util.DeleteBackupSecrets()
+
+ whilte := color.New(color.FgGreen)
+ boldWhite := whilte.Add(color.Bold)
+ time.Sleep(time.Second * 1)
+ boldWhite.Printf(">>>> Welcome to Infisical!")
+ boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email)
+
+ plainBold := color.New(color.Bold)
+
+ plainBold.Println("\nQuick links")
+ fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage")
+ fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack")
+ Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION))
+ } else if loginMethod == "universal-auth" {
+
+ clientId, err := cmd.Flags().GetString("client-id")
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ clientSecret, err := cmd.Flags().GetString("client-secret")
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ if clientId == "" {
+ clientId = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
+ if clientId == "" {
+ util.PrintErrorMessageAndExit("Please provide client-id")
+ }
+ }
+ if clientSecret == "" {
+ clientSecret = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
+ if clientSecret == "" {
+ util.PrintErrorMessageAndExit("Please provide client-secret")
+ }
+ }
+
+ res, err := util.UniversalAuthLogin(clientId, clientSecret)
+
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ if plainOutput {
+ fmt.Println(res.AccessToken)
+ return
+ }
+
+ boldGreen := color.New(color.FgGreen).Add(color.Bold)
+ boldPlain := color.New(color.Bold)
+ time.Sleep(time.Second * 1)
+ boldGreen.Printf(">>>> Successfully authenticated with Universal Auth!\n\n")
+ boldPlain.Printf("Universal Auth Access Token:\n%v", res.AccessToken)
+
+ plainBold := color.New(color.Bold)
+ plainBold.Println("\n\nYou can use this access token to authenticate through other commands in the CLI.")
+
}
-
- err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored)
- if err != nil {
- log.Error().Msgf("Unable to store your credentials in system vault [%s]")
- log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq")
- log.Debug().Err(err)
- //return here
- util.HandleError(err)
- }
-
- err = util.WriteInitalConfig(&userCredentialsToBeStored)
- if err != nil {
- util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
- }
-
- // clear backed up secrets from prev account
- util.DeleteBackupSecrets()
-
- whilte := color.New(color.FgGreen)
- boldWhite := whilte.Add(color.Bold)
- time.Sleep(time.Second * 1)
- boldWhite.Printf(">>>> Welcome to Infisical!")
- boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email)
-
- plainBold := color.New(color.Bold)
-
- plainBold.Println("\nQuick links")
- fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage")
- fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack")
- Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION))
},
}
@@ -301,16 +363,22 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token)
util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info")
}
+ // Login is successful so ask user to choose organization
+ newJwtToken := GetJwtTokenWithOrganizationId(loginTwoResponse.Token)
//updating usercredentials
userCredentialsToBeStored.Email = email
userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey)
- userCredentialsToBeStored.JTWToken = loginTwoResponse.Token
+ userCredentialsToBeStored.JTWToken = newJwtToken
}
func init() {
rootCmd.AddCommand(loginCmd)
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
+ loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
+ loginCmd.Flags().String("client-id", "", "client id for universal auth")
+ loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
+ loginCmd.Flags().String("client-secret", "", "client secret for universal auth")
}
func DomainOverridePrompt() (bool, error) {
@@ -480,6 +548,44 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R
return &loginOneResponseResult, &loginTwoResponseResult, nil
}
+func GetJwtTokenWithOrganizationId(oldJwtToken string) string {
+ log.Debug().Msg(fmt.Sprint("GetJwtTokenWithOrganizationId: ", "oldJwtToken", oldJwtToken))
+
+ httpClient := resty.New()
+ httpClient.SetAuthToken(oldJwtToken)
+
+ organizationResponse, err := api.CallGetAllOrganizations(httpClient)
+
+ if err != nil {
+ util.HandleError(err, "Unable to pull organizations that belong to you")
+ }
+
+ organizations := organizationResponse.Organizations
+
+ organizationNames := util.GetOrganizationsNameList(organizationResponse)
+
+ prompt := promptui.Select{
+ Label: "Which Infisical organization would you like to log into?",
+ Items: organizationNames,
+ }
+
+ index, _, err := prompt.Run()
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ selectedOrganization := organizations[index]
+
+ selectedOrgRes, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID})
+
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ return selectedOrgRes.Token
+
+}
+
func userLoginMenu(currentLoggedInUserEmail string) (bool, error) {
label := fmt.Sprintf("Current logged in user email: %s on domain: %s", currentLoggedInUserEmail, config.INFISICAL_URL)
diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go
index 9c7814ecc..06846260f 100644
--- a/cli/packages/cmd/root.go
+++ b/cli/packages/cmd/root.go
@@ -40,8 +40,14 @@ func init() {
rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)")
rootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary")
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]")
+ rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
- if !util.IsRunningInDocker() {
+ silent, err := cmd.Flags().GetBool("silent")
+ if err != nil {
+ util.HandleError(err)
+ }
+
+ if !util.IsRunningInDocker() && !silent {
util.CheckForUpdate()
}
}
diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go
index d008af1ec..04fe2588b 100644
--- a/cli/packages/cmd/run.go
+++ b/cli/packages/cmd/run.go
@@ -62,8 +62,7 @@ var runCmd = &cobra.Command{
}
}
- infisicalToken, err := util.GetInfisicalServiceToken(cmd)
-
+ token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -73,6 +72,11 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
+ projectId, err := cmd.Flags().GetString("projectId")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
util.HandleError(err, "Unable to parse flag")
@@ -98,7 +102,27 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
- secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, projectConfigDir)
+ recursive, err := cmd.Flags().GetBool("recursive")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
+ request := models.GetAllSecretsParameters{
+ Environment: environmentName,
+ WorkspaceId: projectId,
+ TagSlugs: tagSlugs,
+ SecretsPath: secretsPath,
+ IncludeImport: includeImports,
+ Recursive: recursive,
+ }
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ request.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ request.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir)
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -111,9 +135,16 @@ var runCmd = &cobra.Command{
}
if shouldExpandSecrets {
- secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
- InfisicalToken: infisicalToken,
- }, projectConfigDir)
+
+ authParams := models.ExpandSecretsAuthentication{}
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ authParams.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ authParams.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir)
}
secretsByKey := getSecretsByKeys(secrets)
@@ -144,7 +175,15 @@ var runCmd = &cobra.Command{
log.Debug().Msgf("injecting the following environment variables into shell: %v", env)
- Telemetry.CaptureEvent("cli-command:run", posthog.NewProperties().Set("secretsCount", len(secrets)).Set("environment", environmentName).Set("isUsingServiceToken", infisicalToken != "").Set("single-command", strings.Join(args, " ")).Set("multi-command", cmd.Flag("command").Value.String()).Set("version", util.CLI_VERSION))
+ Telemetry.CaptureEvent("cli-command:run",
+ posthog.NewProperties().
+ Set("secretsCount", len(secrets)).
+ Set("environment", environmentName).
+ Set("isUsingServiceToken", token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER).
+ Set("isUsingUniversalAuthToken", token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER).
+ Set("single-command", strings.Join(args, " ")).
+ Set("multi-command", cmd.Flag("command").Value.String()).
+ Set("version", util.CLI_VERSION))
if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String()
@@ -199,9 +238,11 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) {
func init() {
rootCmd.AddCommand(runCmd)
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
+ runCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
+ runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go
index ffe82179c..7ba6e5411 100644
--- a/cli/packages/cmd/secrets.go
+++ b/cli/packages/cmd/secrets.go
@@ -38,12 +38,12 @@ var secretsCmd = &cobra.Command{
}
}
- infisicalToken, err := util.GetInfisicalServiceToken(cmd)
-
+ token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
+ projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -63,6 +63,11 @@ var secretsCmd = &cobra.Command{
util.HandleError(err)
}
+ recursive, err := cmd.Flags().GetBool("recursive")
+ if err != nil {
+ util.HandleError(err)
+ }
+
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
@@ -73,7 +78,22 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
- secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, "")
+ request := models.GetAllSecretsParameters{
+ Environment: environmentName,
+ WorkspaceId: projectId,
+ TagSlugs: tagSlugs,
+ SecretsPath: secretsPath,
+ IncludeImport: includeImports,
+ Recursive: recursive,
+ }
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ request.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ request.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(request, "")
if err != nil {
util.HandleError(err)
}
@@ -85,9 +105,15 @@ var secretsCmd = &cobra.Command{
}
if shouldExpandSecrets {
- secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{
- InfisicalToken: infisicalToken,
- }, "")
+
+ authParams := models.ExpandSecretsAuthentication{}
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ authParams.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ authParams.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets = util.ExpandSecrets(secrets, authParams, "")
}
visualize.PrintAllSecretDetails(secrets)
@@ -297,7 +323,6 @@ var secretsSetCmd = &cobra.Command{
updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
- SecretName: secret.PlainTextKey,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
@@ -305,7 +330,7 @@ var secretsSetCmd = &cobra.Command{
SecretPath: secretsPath,
}
- err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
+ err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
if err != nil {
util.HandleError(err, "Unable to process secret update request")
return
@@ -398,8 +423,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
}
}
- infisicalToken, err := util.GetInfisicalServiceToken(cmd)
+ token, err := util.GetInfisicalToken(cmd)
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+ shouldExpand, err := cmd.Flags().GetBool("expand")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -409,21 +438,57 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
+ projectId, err := cmd.Flags().GetString("projectId")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse path flag")
}
+ recursive, err := cmd.Flags().GetBool("recursive")
+ if err != nil {
+ util.HandleError(err, "Unable to parse recursive flag")
+ }
+
showOnlyValue, err := cmd.Flags().GetBool("raw-value")
if err != nil {
util.HandleError(err, "Unable to parse path flag")
}
- secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
+ request := models.GetAllSecretsParameters{
+ Environment: environmentName,
+ WorkspaceId: projectId,
+ TagSlugs: tagSlugs,
+ SecretsPath: secretsPath,
+ IncludeImport: true,
+ Recursive: recursive,
+ }
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ request.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ request.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(request, "")
if err != nil {
util.HandleError(err, "To fetch all secrets")
}
+ if shouldExpand {
+ authParams := models.ExpandSecretsAuthentication{}
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ authParams.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ authParams.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets = util.ExpandSecrets(secrets, authParams, "")
+ }
+
requestedSecrets := []models.SingleEnvironmentVariable{}
secretsMap := getSecretsByKeys(secrets)
@@ -466,8 +531,12 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
- infisicalToken, err := util.GetInfisicalServiceToken(cmd)
+ token, err := util.GetInfisicalToken(cmd)
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+ projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -477,7 +546,21 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
- secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
+ request := models.GetAllSecretsParameters{
+ Environment: environmentName,
+ WorkspaceId: projectId,
+ TagSlugs: tagSlugs,
+ SecretsPath: secretsPath,
+ IncludeImport: true,
+ }
+
+ if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
+ request.InfisicalToken = token.Token
+ } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER {
+ request.UniversalAuthAccessToken = token.Token
+ }
+
+ secrets, err := util.GetAllEnvironmentVariables(request, "")
if err != nil {
util.HandleError(err, "To fetch all secrets")
}
@@ -677,18 +760,23 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
func init() {
secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
+ secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path")
secretsCmd.AddCommand(secretsGenerateExampleEnvCmd)
secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
- secretsCmd.AddCommand(secretsGetCmd)
+ secretsGetCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path")
+ secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret")
+ secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
+ secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
secretsCmd.AddCommand(secretsSetCmd)
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
+ // Only supports logged in users (JWT auth)
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
@@ -697,6 +785,8 @@ func init() {
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path")
secretsCmd.AddCommand(secretsDeleteCmd)
+
+ // Only supports logged in users (JWT auth)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
@@ -708,6 +798,7 @@ func init() {
// Add getCmd, createCmd and deleteCmd flags here
getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from")
getCmd.Flags().String("token", "", "Fetch folders using the infisical token")
+ getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
folderCmd.AddCommand(getCmd)
// Add createCmd flags here
@@ -725,9 +816,11 @@ func init() {
// ** End of folders sub command
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
+ secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
+ secretsCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd)
diff --git a/cli/packages/cmd/token.go b/cli/packages/cmd/token.go
new file mode 100644
index 000000000..3e5d42765
--- /dev/null
+++ b/cli/packages/cmd/token.go
@@ -0,0 +1,63 @@
+/*
+Copyright (c) 2023 Infisical Inc.
+*/
+package cmd
+
+import (
+ "strings"
+ "time"
+
+ "github.com/Infisical/infisical-merge/packages/util"
+ "github.com/fatih/color"
+ "github.com/spf13/cobra"
+)
+
+var tokenCmd = &cobra.Command{
+ Use: "token",
+ Short: "Manage your access tokens",
+ DisableFlagsInUseLine: true,
+ Example: "infisical token",
+ Args: cobra.ExactArgs(0),
+ PreRun: func(cmd *cobra.Command, args []string) {
+ util.RequireLogin()
+ },
+ Run: func(cmd *cobra.Command, args []string) {
+ },
+}
+
+var tokenRenewCmd = &cobra.Command{
+ Use: "renew [token]",
+ Short: "Used to renew your universal auth access token",
+ DisableFlagsInUseLine: true,
+ Example: "infisical token renew ",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ // args[0] will be the from your command call
+ token := args[0]
+
+ if strings.HasPrefix(token, "st.") {
+ util.PrintErrorMessageAndExit("You are trying to renew a service token. You can only renew universal auth access tokens.")
+ }
+
+ renewedAccessToken, err := util.RenewUniversalAuthAccessToken(token)
+
+ if err != nil {
+ util.HandleError(err, "Unable to renew token")
+ }
+
+ boldGreen := color.New(color.FgGreen).Add(color.Bold)
+ time.Sleep(time.Second * 1)
+ boldGreen.Printf(">>>> Successfully renewed token!\n\n")
+ boldGreen.Printf("Renewed Access Token:\n%v", renewedAccessToken)
+
+ plainBold := color.New(color.Bold)
+ plainBold.Println("\n\nYou can use the new access token to authenticate through other commands in the CLI.")
+
+ },
+}
+
+func init() {
+ tokenCmd.AddCommand(tokenRenewCmd)
+
+ rootCmd.AddCommand(tokenCmd)
+}
diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go
index 576e74909..68527c469 100644
--- a/cli/packages/models/cli.go
+++ b/cli/packages/models/cli.go
@@ -1,5 +1,7 @@
package models
+import "time"
+
type UserCredentials struct {
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
@@ -40,6 +42,28 @@ type PlaintextSecretResult struct {
Etag string
}
+type DynamicSecret struct {
+ Id string `json:"id"`
+ DefaultTTL string `json:"defaultTTL"`
+ MaxTTL string `json:"maxTTL"`
+ Type string `json:"type"`
+}
+
+type DynamicSecretLease struct {
+ Lease struct {
+ Id string `json:"id"`
+ ExpireAt time.Time `json:"expireAt"`
+ } `json:"lease"`
+ DynamicSecret DynamicSecret `json:"dynamicSecret"`
+ // this is a varying dict based on provider
+ Data map[string]interface{} `json:"data"`
+}
+
+type TokenDetails struct {
+ Type string
+ Token string
+}
+
type SingleFolder struct {
ID string `json:"_id"`
Name string `json:"name"`
@@ -74,13 +98,15 @@ type GetAllSecretsParameters struct {
WorkspaceId string
SecretsPath string
IncludeImport bool
+ Recursive bool
}
type GetAllFoldersParameters struct {
- WorkspaceId string
- Environment string
- FoldersPath string
- InfisicalToken string
+ WorkspaceId string
+ Environment string
+ FoldersPath string
+ InfisicalToken string
+ UniversalAuthAccessToken string
}
type CreateFolderParameters struct {
@@ -103,3 +129,8 @@ type ExpandSecretsAuthentication struct {
InfisicalToken string
UniversalAuthAccessToken string
}
+
+type MachineIdentityCredentials struct {
+ ClientId string
+ ClientSecret string
+}
diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go
index ee2532ee8..311a4b0d9 100644
--- a/cli/packages/util/constants.go
+++ b/cli/packages/util/constants.go
@@ -1,17 +1,23 @@
package util
const (
- CONFIG_FILE_NAME = "infisical-config.json"
- CONFIG_FOLDER_NAME = ".infisical"
- INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api"
- INFISICAL_DEFAULT_URL = "https://app.infisical.com"
- INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
- INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
- SECRET_TYPE_PERSONAL = "personal"
- SECRET_TYPE_SHARED = "shared"
- KEYRING_SERVICE_NAME = "infisical"
- PERSONAL_SECRET_TYPE_NAME = "personal"
- SHARED_SECRET_TYPE_NAME = "shared"
+ CONFIG_FILE_NAME = "infisical-config.json"
+ CONFIG_FOLDER_NAME = ".infisical"
+ INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api"
+ INFISICAL_DEFAULT_URL = "https://app.infisical.com"
+ INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
+ INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
+ INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
+ INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET"
+ INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
+ SECRET_TYPE_PERSONAL = "personal"
+ SECRET_TYPE_SHARED = "shared"
+ KEYRING_SERVICE_NAME = "infisical"
+ PERSONAL_SECRET_TYPE_NAME = "personal"
+ SHARED_SECRET_TYPE_NAME = "shared"
+
+ SERVICE_TOKEN_IDENTIFIER = "service-token"
+ UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
)
var (
diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go
index 0f837ee71..165b97534 100644
--- a/cli/packages/util/folders.go
+++ b/cli/packages/util/folders.go
@@ -19,7 +19,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
var foldersToReturn []models.SingleFolder
var folderErr error
- if params.InfisicalToken == "" {
+ if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" {
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
@@ -44,11 +44,24 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, workspaceFile.WorkspaceId, params.Environment, params.FoldersPath)
folderErr = err
foldersToReturn = folders
- } else {
+ } else if params.InfisicalToken != "" {
+ log.Debug().Msg("GetAllFolders: Trying to fetch folders using service token")
+
// get folders via service token
folders, err := GetFoldersViaServiceToken(params.InfisicalToken, params.WorkspaceId, params.Environment, params.FoldersPath)
folderErr = err
foldersToReturn = folders
+ } else if params.UniversalAuthAccessToken != "" {
+ log.Debug().Msg("GetAllFolders: Trying to fetch folders using universal auth")
+
+ if params.WorkspaceId == "" {
+ PrintErrorMessageAndExit("Project ID is required when using machine identity")
+ }
+
+ // get folders via machine identity
+ folders, err := GetFoldersViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.FoldersPath)
+ folderErr = err
+ foldersToReturn = folders
}
return foldersToReturn, folderErr
}
@@ -132,6 +145,34 @@ func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, envi
return folders, nil
}
+func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlug string, foldersPath string) ([]models.SingleFolder, error) {
+ httpClient := resty.New()
+ httpClient.SetAuthToken(accessToken).
+ SetHeader("Accept", "application/json")
+
+ getFoldersRequest := api.GetFoldersV1Request{
+ WorkspaceId: workspaceId,
+ Environment: envSlug,
+ FoldersPath: foldersPath,
+ }
+
+ apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ var folders []models.SingleFolder
+
+ for _, folder := range apiResponse.Folders {
+ folders = append(folders, models.SingleFolder{
+ Name: folder.Name,
+ ID: folder.ID,
+ })
+ }
+
+ return folders, nil
+}
+
// CreateFolder creates a folder in Infisical
func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) {
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go
index 11b3396dc..e5b8ab6d9 100644
--- a/cli/packages/util/helper.go
+++ b/cli/packages/util/helper.go
@@ -9,8 +9,11 @@ import (
"os/exec"
"path"
"strings"
+ "time"
+ "github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models"
+ "github.com/go-resty/resty/v2"
"github.com/spf13/cobra"
)
@@ -64,18 +67,70 @@ func IsSecretTypeValid(s string) bool {
return false
}
-func GetInfisicalServiceToken(cmd *cobra.Command) (serviceToken string, err error) {
+func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err error) {
infisicalToken, err := cmd.Flags().GetString("token")
- if infisicalToken == "" {
- infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
+ if err != nil {
+ return nil, err
}
+ if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
+ infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
+
+ if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
+ infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
+ }
+ }
+
+ if infisicalToken == "" { // If it's empty, we return nothing at all.
+ return nil, nil
+ }
+
+ if strings.HasPrefix(infisicalToken, "st.") {
+ return &models.TokenDetails{
+ Type: SERVICE_TOKEN_IDENTIFIER,
+ Token: infisicalToken,
+ }, nil
+ }
+
+ return &models.TokenDetails{
+ Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
+ Token: infisicalToken,
+ }, nil
+
+}
+
+func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuthLoginResponse, error) {
+ httpClient := resty.New()
+ httpClient.SetRetryCount(10000).
+ SetRetryMaxWaitTime(20 * time.Second).
+ SetRetryWaitTime(5 * time.Second)
+
+ tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret})
+ if err != nil {
+ return api.UniversalAuthLoginResponse{}, err
+ }
+
+ return tokenResponse, nil
+}
+
+func RenewUniversalAuthAccessToken(accessToken string) (string, error) {
+
+ httpClient := resty.New()
+ httpClient.SetRetryCount(10000).
+ SetRetryMaxWaitTime(20 * time.Second).
+ SetRetryWaitTime(5 * time.Second)
+
+ request := api.UniversalAuthRefreshRequest{
+ AccessToken: accessToken,
+ }
+
+ tokenResponse, err := api.CallUniversalAuthRefreshAccessToken(httpClient, request)
if err != nil {
return "", err
}
- return infisicalToken, nil
+ return tokenResponse.AccessToken, nil
}
// Checks if the passed in email already exists in the users slice
diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go
index 8c142a4ec..59d80ea77 100644
--- a/cli/packages/util/secrets.go
+++ b/cli/packages/util/secrets.go
@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log"
)
-func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
+func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -49,6 +49,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
Environment: environment,
SecretPath: secretPath,
IncludeImport: includeImports,
+ Recursive: recursive,
})
if err != nil {
@@ -80,7 +81,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
return plainTextSecrets, serviceTokenDetails, nil
}
-func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) {
+func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New()
httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
@@ -125,6 +126,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
WorkspaceId: workspaceId,
Environment: environmentName,
IncludeImport: includeImports,
+ Recursive: recursive,
// TagSlugs: tagSlugs,
}
@@ -152,15 +154,16 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
return plainTextSecrets, nil
}
-func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) (models.PlaintextSecretResult, error) {
+func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) {
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
- getSecretsRequest := api.GetEncryptedSecretsV3Request{
+ getSecretsRequest := api.GetRawSecretsV3Request{
WorkspaceId: workspaceId,
Environment: environmentName,
IncludeImport: includeImports,
+ Recursive: recursive,
// TagSlugs: tagSlugs,
}
@@ -168,7 +171,8 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
getSecretsRequest.SecretPath = secretsPath
}
- rawSecrets, err := api.CallGetRawSecretsV3(httpClient, api.GetRawSecretsV3Request{WorkspaceId: workspaceId, SecretPath: secretsPath, Environment: environmentName})
+ rawSecrets, err := api.CallGetRawSecretsV3(httpClient, getSecretsRequest)
+
if err != nil {
return models.PlaintextSecretResult{}, err
}
@@ -179,7 +183,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
}
for _, secret := range rawSecrets.Secrets {
- plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, WorkspaceId: secret.Workspace})
+ plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace})
}
// if includeImports {
@@ -195,6 +199,31 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
}, nil
}
+func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) {
+ httpClient := resty.New()
+ httpClient.SetAuthToken(accessToken).
+ SetHeader("Accept", "application/json")
+
+ dynamicSecretRequest := api.CreateDynamicSecretLeaseV1Request{
+ ProjectSlug: projectSlug,
+ Environment: environmentName,
+ SecretPath: secretsPath,
+ Slug: slug,
+ TTL: ttl,
+ }
+
+ dynamicSecret, err := api.CallCreateDynamicSecretLeaseV1(httpClient, dynamicSecretRequest)
+ if err != nil {
+ return models.DynamicSecretLease{}, err
+ }
+
+ return models.DynamicSecretLease{
+ Lease: dynamicSecret.Lease,
+ Data: dynamicSecret.Data,
+ DynamicSecret: dynamicSecret.DynamicSecret,
+ }, nil
+}
+
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil
@@ -304,7 +333,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
}
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId,
- params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport)
+ params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport, params.Recursive)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -325,10 +354,15 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} else {
if params.InfisicalToken != "" {
log.Debug().Msg("Trying to fetch secrets using service token")
- secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
+ secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
} else if params.UniversalAuthAccessToken != "" {
+
+ if params.WorkspaceId == "" {
+ PrintErrorMessageAndExit("Project ID is required when using machine identity")
+ }
+
log.Debug().Msg("Trying to fetch secrets using universal auth")
- res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport)
+ res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
errorToReturn = err
secretsToReturn = res.Secrets
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index f07aeb190..764761098 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -66,6 +66,8 @@ services:
environment:
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
command: npm run migration:latest
+ volumes:
+ - ./backend/src:/app/src
backend:
container_name: infisical-dev-api
diff --git a/docs/api-reference/endpoints/folders/delete.mdx b/docs/api-reference/endpoints/folders/delete.mdx
index dc73a41da..a106cc2eb 100644
--- a/docs/api-reference/endpoints/folders/delete.mdx
+++ b/docs/api-reference/endpoints/folders/delete.mdx
@@ -1,4 +1,4 @@
---
title: "Delete"
-openapi: "DELETE /api/v1/folders/{folderId}"
+openapi: "DELETE /api/v1/folders/{folderIdOrName}"
---
diff --git a/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx b/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx
new file mode 100644
index 000000000..8e02c28a3
--- /dev/null
+++ b/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx
@@ -0,0 +1,4 @@
+---
+title: "Create Permanent"
+openapi: "POST /api/v1/additional-privilege/identity/permanent"
+---
diff --git a/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx b/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx
new file mode 100644
index 000000000..808f27859
--- /dev/null
+++ b/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx
@@ -0,0 +1,4 @@
+---
+title: "Create Temporary"
+openapi: "POST /api/v1/additional-privilege/identity/temporary"
+---
diff --git a/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx b/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx
new file mode 100644
index 000000000..430282789
--- /dev/null
+++ b/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete"
+openapi: "DELETE /api/v1/additional-privilege/identity"
+---
diff --git a/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx b/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx
new file mode 100644
index 000000000..a6ec27217
--- /dev/null
+++ b/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx
@@ -0,0 +1,4 @@
+---
+title: "Find By Privilege Slug"
+openapi: "GET /api/v1/additional-privilege/identity/{privilegeSlug}"
+---
diff --git a/docs/api-reference/endpoints/identity-specific-privilege/list.mdx b/docs/api-reference/endpoints/identity-specific-privilege/list.mdx
new file mode 100644
index 000000000..4698ed838
--- /dev/null
+++ b/docs/api-reference/endpoints/identity-specific-privilege/list.mdx
@@ -0,0 +1,4 @@
+---
+title: "List"
+openapi: "GET /api/v1/additional-privilege/identity"
+---
diff --git a/docs/api-reference/endpoints/identity-specific-privilege/update.mdx b/docs/api-reference/endpoints/identity-specific-privilege/update.mdx
new file mode 100644
index 000000000..987d6ac8c
--- /dev/null
+++ b/docs/api-reference/endpoints/identity-specific-privilege/update.mdx
@@ -0,0 +1,4 @@
+---
+title: "Update"
+openapi: "PATCH /api/v1/additional-privilege/identity"
+---
diff --git a/docs/api-reference/endpoints/integrations/create-auth.mdx b/docs/api-reference/endpoints/integrations/create-auth.mdx
new file mode 100644
index 000000000..5af7a0f9c
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/create-auth.mdx
@@ -0,0 +1,32 @@
+---
+title: "Create Auth"
+openapi: "POST /api/v1/integration-auth/access-token"
+---
+
+## Integration Authentication Parameters
+
+The integration authentication endpoint is generic and can be used for all native integrations.
+For specific integration parameters for a given service, please review the respective documentation below.
+
+
+
+
+ This value must be **aws-secret-manager**.
+
+
+ Infisical project id for the integration.
+
+
+ The AWS IAM User Access ID.
+
+
+ The AWS IAM User Access Secret Key.
+
+
+
+ Coming Soon
+
+
+ Coming Soon
+
+
diff --git a/docs/api-reference/endpoints/integrations/create.mdx b/docs/api-reference/endpoints/integrations/create.mdx
new file mode 100644
index 000000000..0992e91b9
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/create.mdx
@@ -0,0 +1,40 @@
+---
+title: "Create"
+openapi: "POST /api/v1/integration"
+---
+
+## Integration Parameters
+
+The integration creation endpoint is generic and can be used for all native integrations.
+For specific integration parameters for a given service, please review the respective documentation below.
+
+
+
+
+ The ID of the integration auth object for authentication with AWS.
+ Refer [Create Integration Auth](./create-auth) for more info
+
+
+ Whether the integration should be active or inactive
+
+
+ The secret name used when saving secret in AWS SSM. Used for naming and can be arbitrary.
+
+
+ The AWS region of the SSM. Example: `us-east-1`
+
+
+ The Infisical environment slug from where secrets will be synced from. Example: `dev`
+
+
+ The Infisical folder path from where secrets will be synced from. Example: `/some/path`. The root of the environment is `/`.
+
+
+
+ Coming Soon
+
+
+ Coming Soon
+
+
+
diff --git a/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx b/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx
new file mode 100644
index 000000000..5884363fc
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete Auth By ID"
+openapi: "DELETE /api/v1/integration-auth/{integrationAuthId}"
+---
diff --git a/docs/api-reference/endpoints/integrations/delete-auth.mdx b/docs/api-reference/endpoints/integrations/delete-auth.mdx
new file mode 100644
index 000000000..93d957903
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/delete-auth.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete Auth"
+openapi: "DELETE /api/v1/integration-auth"
+---
diff --git a/docs/api-reference/endpoints/integrations/delete.mdx b/docs/api-reference/endpoints/integrations/delete.mdx
new file mode 100644
index 000000000..51df56de7
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/delete.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete"
+openapi: "DELETE /api/v1/integration/{integrationId}"
+---
diff --git a/docs/api-reference/endpoints/integrations/find-auth.mdx b/docs/api-reference/endpoints/integrations/find-auth.mdx
new file mode 100644
index 000000000..439b82935
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/find-auth.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get Auth By ID"
+openapi: "GET /api/v1/integration-auth/{integrationAuthId}"
+---
diff --git a/docs/api-reference/endpoints/integrations/list-auth.mdx b/docs/api-reference/endpoints/integrations/list-auth.mdx
new file mode 100644
index 000000000..3ca961d98
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/list-auth.mdx
@@ -0,0 +1,4 @@
+---
+title: "List Auth"
+openapi: "GET /api/v1/workspace/{workspaceId}/authorizations"
+---
diff --git a/docs/api-reference/endpoints/integrations/list-project-integrations.mdx b/docs/api-reference/endpoints/integrations/list-project-integrations.mdx
new file mode 100644
index 000000000..24ebbf7d8
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/list-project-integrations.mdx
@@ -0,0 +1,4 @@
+---
+title: "List Project Integrations"
+openapi: "GET /api/v1/workspace/{workspaceId}/integrations"
+---
diff --git a/docs/api-reference/endpoints/integrations/update.mdx b/docs/api-reference/endpoints/integrations/update.mdx
new file mode 100644
index 000000000..8567c46ae
--- /dev/null
+++ b/docs/api-reference/endpoints/integrations/update.mdx
@@ -0,0 +1,4 @@
+---
+title: "Update"
+openapi: "PATCH /api/v1/integration/{integrationId}"
+---
diff --git a/docs/api-reference/endpoints/secret-tags/create.mdx b/docs/api-reference/endpoints/secret-tags/create.mdx
new file mode 100644
index 000000000..82d0eed17
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-tags/create.mdx
@@ -0,0 +1,4 @@
+---
+title: "Create"
+openapi: "POST /api/v1/workspace/{projectId}/tags"
+---
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/secret-tags/delete.mdx b/docs/api-reference/endpoints/secret-tags/delete.mdx
new file mode 100644
index 000000000..cc98f03c2
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-tags/delete.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete"
+openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}"
+---
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/secret-tags/list.mdx b/docs/api-reference/endpoints/secret-tags/list.mdx
new file mode 100644
index 000000000..c4a940f77
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-tags/list.mdx
@@ -0,0 +1,4 @@
+---
+title: "List"
+openapi: "GET /api/v1/workspace/{projectId}/tags"
+---
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/secrets/attach-tags.mdx b/docs/api-reference/endpoints/secrets/attach-tags.mdx
new file mode 100644
index 000000000..8dd0e6081
--- /dev/null
+++ b/docs/api-reference/endpoints/secrets/attach-tags.mdx
@@ -0,0 +1,4 @@
+---
+title: "Attach tags"
+openapi: "POST /api/v3/secrets/tags/{secretName}"
+---
diff --git a/docs/api-reference/endpoints/secrets/detach-tags.mdx b/docs/api-reference/endpoints/secrets/detach-tags.mdx
new file mode 100644
index 000000000..a74b1174e
--- /dev/null
+++ b/docs/api-reference/endpoints/secrets/detach-tags.mdx
@@ -0,0 +1,4 @@
+---
+title: "Detach tags"
+openapi: "DELETE /api/v3/secrets/tags/{secretName}"
+---
\ No newline at end of file
diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx
index dcf9719ea..f2224577e 100644
--- a/docs/api-reference/overview/authentication.mdx
+++ b/docs/api-reference/overview/authentication.mdx
@@ -1,9 +1,9 @@
---
title: "Authentication"
-description: "How to authenticate with the Infisical Public API"
+description: "Learn how to authenticate with the Infisical Public API."
---
-You can authenticate with the Infisical API using [Identities](/documentation/platform/identities/overview) paired with authentication modes such as [Universal Auth](/documentation/platform/identities/universal-auth).
+You can authenticate with the Infisical API using [Identities](/documentation/platform/identities/machine-identities) paired with authentication modes such as [Universal Auth](/documentation/platform/identities/universal-auth).
To interact with the Infisical API, you will need to obtain an access token. Follow the step by [step guide](/documentation/platform/identities/universal-auth) to get an access token via Universal Auth.
diff --git a/docs/api-reference/overview/examples/e2ee-disabled.mdx b/docs/api-reference/overview/examples/e2ee-disabled.mdx
deleted file mode 100644
index 1a9e57552..000000000
--- a/docs/api-reference/overview/examples/e2ee-disabled.mdx
+++ /dev/null
@@ -1,180 +0,0 @@
----
-title: "E2EE Disabled"
----
-
-Using Infisical's API to read/write secrets with E2EE disabled allows you to create, update, and retrieve secrets
-in plaintext. Effectively, this means each such secret operation only requires 1 HTTP call.
-
-
-
- Retrieve all secrets for an Infisical project and environment.
-
-
- ```bash
- curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw?environment=environment&workspaceId=workspaceId' \
- --header 'Authorization: Bearer serviceToken'
-
- ```
-
-
- ####
-
- When using a [service token](../../../documentation/platform/token) with access to a single environment and path, you don't need to provide request parameters because the server will automatically scope the request to the defined environment/secrets path of the service token used.
- For all other cases, request parameters are required.
-
- ####
-
- The ID of the workspace
-
-
- The environment slug
-
-
- Path to secrets in workspace
-
-
-
- Create a secret in Infisical.
-
-
-
- ```bash
- curl --location --request POST 'https://app.infisical.com/api/v3/secrets/raw/secretName' \
- --header 'Authorization: Bearer serviceToken' \
- --header 'Content-Type: application/json' \
- --data-raw '{
- "workspaceId": "workspaceId",
- "environment": "environment",
- "type": "shared",
- "secretValue": "secretValue",
- "secretPath": "/"
- }'
- ```
-
-
-
-
- Name of secret to create
-
-
- The ID of the workspace
-
-
- The environment slug
-
-
- Value of secret
-
-
- Comment of secret
-
-
- Path to secret in workspace
-
-
- The type of the secret. Valid options are โsharedโ or โpersonalโ
-
-
-
- Retrieve a secret from Infisical.
-
-
-
- ```bash
- curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \
- --header 'Authorization: Bearer serviceToken'
- ```
-
-
-
-
- Name of secret to retrieve
-
-
- The ID of the workspace
-
-
- The environment slug
-
-
- Path to secrets in workspace
-
-
- The type of the secret. Valid options are โsharedโ or โpersonalโ
-
-
-
- Update an existing secret in Infisical.
-
-
-
- ```bash
- curl --location --request PATCH 'https://app.infisical.com/api/v3/secrets/raw/secretName' \
- --header 'Authorization: Bearer serviceToken' \
- --header 'Content-Type: application/json' \
- --data-raw '{
- "workspaceId": "workspaceId",
- "environment": "environment",
- "type": "shared",
- "secretValue": "secretValue",
- "secretPath": "/"
- }'
- ```
-
-
-
-
- Name of secret to update
-
-
- The ID of the workspace
-
-
- The environment slug
-
-
- Value of secret
-
-
- Path to secret in workspace.
-
-
- The type of the secret. Valid options are โsharedโ or โpersonalโ
-
-
-
- Delete a secret in Infisical.
-
-
-
- ```bash
- curl --location --request DELETE 'https://app.infisical.com/api/v3/secrets/raw/secretName' \
- --header 'Authorization: Bearer serviceToken' \
- --header 'Content-Type: application/json' \
- --data-raw '{
- "workspaceId": "workspaceId",
- "environment": "environment",
- "type": "shared",
- "secretPath": "/"
- }'
- ```
-
-
-
-
- Name of secret to update
-
-
- The ID of the workspace
-
-
- The environment slug
-
-
- Path to secret in workspace.
-
-
- The type of the secret. Valid options are โsharedโ or โpersonalโ
-
-
-
\ No newline at end of file
diff --git a/docs/api-reference/overview/examples/e2ee-enabled.mdx b/docs/api-reference/overview/examples/e2ee-enabled.mdx
deleted file mode 100644
index 1de9c2290..000000000
--- a/docs/api-reference/overview/examples/e2ee-enabled.mdx
+++ /dev/null
@@ -1,862 +0,0 @@
----
-title: "E2EE Enabled"
----
-
-
- E2EE enabled mode only works with [Service Tokens](/documentation/platform/token) and cannot be used with [Identities](/documentation/platform/identities/overview).
-
-
-Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets
-but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available
-SDKs instead.
-
-
-
-
-
- Retrieve all secrets for an Infisical project and environment.
-```js
-const crypto = require('crypto');
-const axios = require('axios');
-
-const BASE_URL = 'https://app.infisical.com';
-const ALGORITHM = 'aes-256-gcm';
-
-const decrypt = ({ ciphertext, iv, tag, secret}) => {
- const decipher = crypto.createDecipheriv(
- ALGORITHM,
- secret,
- Buffer.from(iv, 'base64')
- );
- decipher.setAuthTag(Buffer.from(tag, 'base64'));
-
- let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
- cleartext += decipher.final('utf8');
-
- return cleartext;
-}
-
-const getSecrets = async () => {
- const serviceToken = 'your_service_token';
- const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
-
- // 1. Get your Infisical Token data
- const { data: serviceTokenData } = await axios.get(
- `${BASE_URL}/api/v2/service-token`,
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-
- // 2. Get secrets for your project and environment
- const { data } = await axios.get(
- `${BASE_URL}/api/v3/secrets?${new URLSearchParams({
- environment: serviceTokenData.environment,
- workspaceId: serviceTokenData.workspace
- })}`,
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-
- const encryptedSecrets = data.secrets;
-
- // 3. Decrypt the (encrypted) project key with the key from your Infisical Token
- const projectKey = decrypt({
- ciphertext: serviceTokenData.encryptedKey,
- iv: serviceTokenData.iv,
- tag: serviceTokenData.tag,
- secret: serviceTokenSecret
- });
-
- // 4. Decrypt the (encrypted) secrets
- const secrets = encryptedSecrets.map((secret) => {
- const secretKey = decrypt({
- ciphertext: secret.secretKeyCiphertext,
- iv: secret.secretKeyIV,
- tag: secret.secretKeyTag,
- secret: projectKey
- });
-
- const secretValue = decrypt({
- ciphertext: secret.secretValueCiphertext,
- iv: secret.secretValueIV,
- tag: secret.secretValueTag,
- secret: projectKey
- });
-
- return ({
- secretKey,
- secretValue
- });
- });
-
- console.log('secrets: ', secrets);
-}
-
-getSecrets();
-
-```
-
-
-
-```Python
-import requests
-import base64
-from Cryptodome.Cipher import AES
-
-
-BASE_URL = "http://app.infisical.com"
-
-
-def decrypt(ciphertext, iv, tag, secret):
- secret = bytes(secret, "utf-8")
- iv = base64.standard_b64decode(iv)
- tag = base64.standard_b64decode(tag)
- ciphertext = base64.standard_b64decode(ciphertext)
-
- cipher = AES.new(secret, AES.MODE_GCM, iv)
- cipher.update(tag)
- cleartext = cipher.decrypt(ciphertext).decode("utf-8")
- return cleartext
-
-
-def get_secrets():
- service_token = "your_service_token"
- service_token_secret = service_token[service_token.rindex(".") + 1 :]
-
- # 1. Get your Infisical Token data
- service_token_data = requests.get(
- f"{BASE_URL}/api/v2/service-token",
- headers={"Authorization": f"Bearer {service_token}"},
- ).json()
-
- # 2. Get secrets for your project and environment
- data = requests.get(
- f"{BASE_URL}/api/v3/secrets",
- params={
- "environment": service_token_data["environment"],
- "workspaceId": service_token_data["workspace"],
- },
- headers={"Authorization": f"Bearer {service_token}"},
- ).json()
-
- encrypted_secrets = data["secrets"]
-
- # 3. Decrypt the (encrypted) project key with the key from your Infisical Token
- project_key = decrypt(
- ciphertext=service_token_data["encryptedKey"],
- iv=service_token_data["iv"],
- tag=service_token_data["tag"],
- secret=service_token_secret,
- )
-
- # 4. Decrypt the (encrypted) secrets
- secrets = []
- for secret in encrypted_secrets:
- secret_key = decrypt(
- ciphertext=secret["secretKeyCiphertext"],
- iv=secret["secretKeyIV"],
- tag=secret["secretKeyTag"],
- secret=project_key,
- )
-
- secret_value = decrypt(
- ciphertext=secret["secretValueCiphertext"],
- iv=secret["secretValueIV"],
- tag=secret["secretValueTag"],
- secret=project_key,
- )
-
- secrets.append(
- {
- "secret_key": secret_key,
- "secret_value": secret_value,
- }
- )
-
- print("secrets:", secrets)
-
-
-get_secrets()
-
-```
-
-
-
-
-
-
-Create a secret in Infisical.
-```js
-const crypto = require('crypto');
-const axios = require('axios');
-const nacl = require('tweetnacl');
-
-const BASE_URL = 'https://app.infisical.com';
-const ALGORITHM = 'aes-256-gcm';
-const BLOCK_SIZE_BYTES = 16;
-
-const encrypt = ({ text, secret }) => {
- const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
- const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
-
- let ciphertext = cipher.update(text, 'utf8', 'base64');
- ciphertext += cipher.final('base64');
- return {
- ciphertext,
- iv: iv.toString('base64'),
- tag: cipher.getAuthTag().toString('base64')
- };
-}
-
-const decrypt = ({ ciphertext, iv, tag, secret}) => {
- const decipher = crypto.createDecipheriv(
- ALGORITHM,
- secret,
- Buffer.from(iv, 'base64')
- );
- decipher.setAuthTag(Buffer.from(tag, 'base64'));
-
- let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
- cleartext += decipher.final('utf8');
-
- return cleartext;
-}
-
-const createSecrets = async () => {
- const serviceToken = '';
- const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
-
- const secretType = 'shared'; // 'shared' or 'personal'
- const secretKey = 'some_key';
- const secretValue = 'some_value';
- const secretComment = 'some_comment';
-
- // 1. Get your Infisical Token data
- const { data: serviceTokenData } = await axios.get(
- `${BASE_URL}/api/v2/service-token`,
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-
- // 2. Decrypt the (encrypted) project key with the key from your Infisical Token
- const projectKey = decrypt({
- ciphertext: serviceTokenData.encryptedKey,
- iv: serviceTokenData.iv,
- tag: serviceTokenData.tag,
- secret: serviceTokenSecret
- });
-
- // 3. Encrypt your secret with the project key
- const {
- ciphertext: secretKeyCiphertext,
- iv: secretKeyIV,
- tag: secretKeyTag
- } = encrypt({
- text: secretKey,
- secret: projectKey
- });
-
- const {
- ciphertext: secretValueCiphertext,
- iv: secretValueIV,
- tag: secretValueTag
- } = encrypt({
- text: secretValue,
- secret: projectKey
- });
-
- const {
- ciphertext: secretCommentCiphertext,
- iv: secretCommentIV,
- tag: secretCommentTag
- } = encrypt({
- text: secretComment,
- secret: projectKey
- });
-
- // 4. Send (encrypted) secret to Infisical
- await axios.post(
- `${BASE_URL}/api/v3/secrets/${secretKey}`,
- {
- workspaceId: serviceTokenData.workspace,
- environment: serviceTokenData.environment,
- type: secretType,
- secretKeyCiphertext,
- secretKeyIV,
- secretKeyTag,
- secretValueCiphertext,
- secretValueIV,
- secretValueTag,
- secretCommentCiphertext,
- secretCommentIV,
- secretCommentTag
- },
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-}
-
-createSecrets();
-```
-
-
-
-```Python
-import base64
-import requests
-from Cryptodome.Cipher import AES
-from Cryptodome.Random import get_random_bytes
-
-
-BASE_URL = "https://app.infisical.com"
-BLOCK_SIZE_BYTES = 16
-
-
-def encrypt(text, secret):
- iv = get_random_bytes(BLOCK_SIZE_BYTES)
- secret = bytes(secret, "utf-8")
- cipher = AES.new(secret, AES.MODE_GCM, iv)
- ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
- return {
- "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
- "tag": base64.standard_b64encode(tag).decode("utf-8"),
- "iv": base64.standard_b64encode(iv).decode("utf-8"),
- }
-
-
-def decrypt(ciphertext, iv, tag, secret):
- secret = bytes(secret, "utf-8")
- iv = base64.standard_b64decode(iv)
- tag = base64.standard_b64decode(tag)
- ciphertext = base64.standard_b64decode(ciphertext)
-
- cipher = AES.new(secret, AES.MODE_GCM, iv)
- cipher.update(tag)
- cleartext = cipher.decrypt(ciphertext).decode("utf-8")
- return cleartext
-
-
-def create_secrets():
- service_token = "your_service_token"
- service_token_secret = service_token[service_token.rindex(".") + 1 :]
-
- secret_type = "shared" # "shared or "personal"
- secret_key = "some_key"
- secret_value = "some_value"
- secret_comment = "some_comment"
-
- # 1. Get your Infisical Token data
- service_token_data = requests.get(
- f"{BASE_URL}/api/v2/service-token",
- headers={"Authorization": f"Bearer {service_token}"},
- ).json()
-
- # 2. Decrypt the (encrypted) project key with the key from your Infisical Token
- project_key = decrypt(
- ciphertext=service_token_data["encryptedKey"],
- iv=service_token_data["iv"],
- tag=service_token_data["tag"],
- secret=service_token_secret,
- )
-
- # 3. Encrypt your secret with the project key
- encrypted_key_data = encrypt(text=secret_key, secret=project_key)
- encrypted_value_data = encrypt(text=secret_value, secret=project_key)
- encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
-
- # 4. Send (encrypted) secret to Infisical
- requests.post(
- f"{BASE_URL}/api/v3/secrets/{secret_key}",
- json={
- "workspaceId": service_token_data["workspace"],
- "environment": service_token_data["environment"],
- "type": secret_type,
- "secretKeyCiphertext": encrypted_key_data["ciphertext"],
- "secretKeyIV": encrypted_key_data["iv"],
- "secretKeyTag": encrypted_key_data["tag"],
- "secretValueCiphertext": encrypted_value_data["ciphertext"],
- "secretValueIV": encrypted_value_data["iv"],
- "secretValueTag": encrypted_value_data["tag"],
- "secretCommentCiphertext": encrypted_comment_data["ciphertext"],
- "secretCommentIV": encrypted_comment_data["iv"],
- "secretCommentTag": encrypted_comment_data["tag"]
- },
- headers={"Authorization": f"Bearer {service_token}"},
- )
-
-
-create_secrets()
-
-```
-
-
-
-
-
-
- Retrieve a secret from Infisical.
-```js
-const crypto = require('crypto');
-const axios = require('axios');
-
-const BASE_URL = 'https://app.infisical.com';
-const ALGORITHM = 'aes-256-gcm';
-
-const decrypt = ({ ciphertext, iv, tag, secret}) => {
- const decipher = crypto.createDecipheriv(
- ALGORITHM,
- secret,
- Buffer.from(iv, 'base64')
- );
- decipher.setAuthTag(Buffer.from(tag, 'base64'));
-
- let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
- cleartext += decipher.final('utf8');
-
- return cleartext;
-}
-
-const getSecret = async () => {
- const serviceToken = 'your_service_token';
- const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
-
- const secretType = 'shared' // 'shared' or 'personal'
- const secretKey = 'some_key';
-
- // 1. Get your Infisical Token data
- const { data: serviceTokenData } = await axios.get(
- `${BASE_URL}/api/v2/service-token`,
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-
- // 2. Get the secret from your project and environment
- const { data } = await axios.get(
- `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({
- environment: serviceTokenData.environment,
- workspaceId: serviceTokenData.workspace,
- type: secretType // optional, defaults to 'shared'
- })}`,
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-
- const encryptedSecret = data.secret;
-
- // 3. Decrypt the (encrypted) project key with the key from your Infisical Token
- const projectKey = decrypt({
- ciphertext: serviceTokenData.encryptedKey,
- iv: serviceTokenData.iv,
- tag: serviceTokenData.tag,
- secret: serviceTokenSecret
- });
-
- // 4. Decrypt the (encrypted) secret value
-
- const secretValue = decrypt({
- ciphertext: encryptedSecret.secretValueCiphertext,
- iv: encryptedSecret.secretValueIV,
- tag: encryptedSecret.secretValueTag,
- secret: projectKey
- });
-
- console.log('secret: ', ({
- secretKey,
- secretValue
- }));
-}
-
-getSecret();
-
-```
-
-
-
-```Python
-import requests
-import base64
-from Cryptodome.Cipher import AES
-
-
-BASE_URL = "http://app.infisical.com"
-
-
-def decrypt(ciphertext, iv, tag, secret):
- secret = bytes(secret, "utf-8")
- iv = base64.standard_b64decode(iv)
- tag = base64.standard_b64decode(tag)
- ciphertext = base64.standard_b64decode(ciphertext)
-
- cipher = AES.new(secret, AES.MODE_GCM, iv)
- cipher.update(tag)
- cleartext = cipher.decrypt(ciphertext).decode("utf-8")
- return cleartext
-
-
-def get_secret():
- service_token = "your_service_token"
- service_token_secret = service_token[service_token.rindex(".") + 1 :]
-
- secret_type = "shared" # "shared" or "personal"
- secret_key = "some_key"
-
- # 1. Get your Infisical Token data
- service_token_data = requests.get(
- f"{BASE_URL}/api/v2/service-token",
- headers={"Authorization": f"Bearer {service_token}"},
- ).json()
-
- # 2. Get secret from your project and environment
- data = requests.get(
- f"{BASE_URL}/api/v3/secrets/{secret_key}",
- params={
- "environment": service_token_data["environment"],
- "workspaceId": service_token_data["workspace"],
- "type": secret_type # optional, defaults to "shared"
- },
- headers={"Authorization": f"Bearer {service_token}"},
- ).json()
-
- encrypted_secret = data["secret"]
-
- # 3. Decrypt the (encrypted) project key with the key from your Infisical Token
- project_key = decrypt(
- ciphertext=service_token_data["encryptedKey"],
- iv=service_token_data["iv"],
- tag=service_token_data["tag"],
- secret=service_token_secret,
- )
-
- # 4. Decrypt the (encrypted) secret value
- secret_value = decrypt(
- ciphertext=encrypted_secret["secretValueCiphertext"],
- iv=encrypted_secret["secretValueIV"],
- tag=encrypted_secret["secretValueTag"],
- secret=project_key,
- )
-
- print("secret: ", {
- "secret_key": secret_key,
- "secret_value": secret_value
- })
-
-
-get_secret()
-
-```
-
-
-
-
-
-
-Update an existing secret in Infisical.
-```js
-const crypto = require('crypto');
-const axios = require('axios');
-
-const BASE_URL = 'https://app.infisical.com';
-const ALGORITHM = 'aes-256-gcm';
-const BLOCK_SIZE_BYTES = 16;
-
-const encrypt = ({ text, secret }) => {
- const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
- const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
-
- let ciphertext = cipher.update(text, 'utf8', 'base64');
- ciphertext += cipher.final('base64');
- return {
- ciphertext,
- iv: iv.toString('base64'),
- tag: cipher.getAuthTag().toString('base64')
- };
-}
-
-const decrypt = ({ ciphertext, iv, tag, secret}) => {
- const decipher = crypto.createDecipheriv(
- ALGORITHM,
- secret,
- Buffer.from(iv, 'base64')
- );
- decipher.setAuthTag(Buffer.from(tag, 'base64'));
-
- let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
- cleartext += decipher.final('utf8');
-
- return cleartext;
-}
-
-const updateSecrets = async () => {
- const serviceToken = 'your_service_token';
- const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
-
- const secretType = 'shared' // 'shared' or 'personal'
- const secretKey = 'some_key';
- const secretValue = 'updated_value';
- const secretComment = 'updated_comment';
-
- // 1. Get your Infisical Token data
- const { data: serviceTokenData } = await axios.get(
- `${BASE_URL}/api/v2/service-token`,
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-
- // 2. Decrypt the (encrypted) project key with the key from your Infisical Token
- const projectKey = decrypt({
- ciphertext: serviceTokenData.encryptedKey,
- iv: serviceTokenData.iv,
- tag: serviceTokenData.tag,
- secret: serviceTokenSecret
- });
-
- // 3. Encrypt your updated secret with the project key
- const {
- ciphertext: secretKeyCiphertext,
- iv: secretKeyIV,
- tag: secretKeyTag
- } = encrypt({
- text: secretKey,
- secret: projectKey
- });
-
- const {
- ciphertext: secretValueCiphertext,
- iv: secretValueIV,
- tag: secretValueTag
- } = encrypt({
- text: secretValue,
- secret: projectKey
- });
-
- const {
- ciphertext: secretCommentCiphertext,
- iv: secretCommentIV,
- tag: secretCommentTag
- } = encrypt({
- text: secretComment,
- secret: projectKey
- });
-
- // 4. Send (encrypted) updated secret to Infisical
- await axios.patch(
- `${BASE_URL}/api/v3/secrets/${secretKey}`,
- {
- workspaceId: serviceTokenData.workspace,
- environment: serviceTokenData.environment,
- type: secretType,
- secretValueCiphertext,
- secretValueIV,
- secretValueTag,
- secretCommentCiphertext,
- secretCommentIV,
- secretCommentTag
- },
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-}
-
-updateSecrets();
-```
-
-
-
-```Python
-import base64
-import requests
-from Cryptodome.Cipher import AES
-from Cryptodome.Random import get_random_bytes
-
-
-BASE_URL = "https://app.infisical.com"
-BLOCK_SIZE_BYTES = 16
-
-
-def encrypt(text, secret):
- iv = get_random_bytes(BLOCK_SIZE_BYTES)
- secret = bytes(secret, "utf-8")
- cipher = AES.new(secret, AES.MODE_GCM, iv)
- ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8"))
- return {
- "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"),
- "tag": base64.standard_b64encode(tag).decode("utf-8"),
- "iv": base64.standard_b64encode(iv).decode("utf-8"),
- }
-
-
-def decrypt(ciphertext, iv, tag, secret):
- secret = bytes(secret, "utf-8")
- iv = base64.standard_b64decode(iv)
- tag = base64.standard_b64decode(tag)
- ciphertext = base64.standard_b64decode(ciphertext)
-
- cipher = AES.new(secret, AES.MODE_GCM, iv)
- cipher.update(tag)
- cleartext = cipher.decrypt(ciphertext).decode("utf-8")
- return cleartext
-
-
-def update_secret():
- service_token = "your_service_token"
- service_token_secret = service_token[service_token.rindex(".") + 1 :]
-
- secret_type = "shared" # "shared" or "personal"
- secret_key = "some_key"
- secret_value = "updated_value"
- secret_comment = "updated_comment"
-
- # 1. Get your Infisical Token data
- service_token_data = requests.get(
- f"{BASE_URL}/api/v2/service-token",
- headers={"Authorization": f"Bearer {service_token}"},
- ).json()
-
- # 2. Decrypt the (encrypted) project key with the key from your Infisical Token
- project_key = decrypt(
- ciphertext=service_token_data["encryptedKey"],
- iv=service_token_data["iv"],
- tag=service_token_data["tag"],
- secret=service_token_secret,
- )
-
- # 3. Encrypt your updated secret with the project key
- encrypted_key_data = encrypt(text=secret_key, secret=project_key)
- encrypted_value_data = encrypt(text=secret_value, secret=project_key)
- encrypted_comment_data = encrypt(text=secret_comment, secret=project_key)
-
- # 4. Send (encrypted) updated secret to Infisical
- requests.patch(
- f"{BASE_URL}/api/v3/secrets/{secret_key}",
- json={
- "workspaceId": service_token_data["workspace"],
- "environment": service_token_data["environment"],
- "type": secret_type,
- "secretKeyCiphertext": encrypted_key_data["ciphertext"],
- "secretKeyIV": encrypted_key_data["iv"],
- "secretKeyTag": encrypted_key_data["tag"],
- "secretValueCiphertext": encrypted_value_data["ciphertext"],
- "secretValueIV": encrypted_value_data["iv"],
- "secretValueTag": encrypted_value_data["tag"],
- "secretCommentCiphertext": encrypted_comment_data["ciphertext"],
- "secretCommentIV": encrypted_comment_data["iv"],
- "secretCommentTag": encrypted_comment_data["tag"]
- },
- headers={"Authorization": f"Bearer {service_token}"},
- )
-
-
-update_secret()
-
-```
-
-
-
-
-
-
- Delete a secret in Infisical.
-```js
-const axios = require('axios');
-const BASE_URL = 'https://app.infisical.com';
-
-const deleteSecrets = async () => {
- const serviceToken = 'your_service_token';
- const secretType = 'shared' // 'shared' or 'personal'
- const secretKey = 'some_key'
-
- // 1. Get your Infisical Token data
- const { data: serviceTokenData } = await axios.get(
- `${BASE_URL}/api/v2/service-token`,
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- }
- }
- );
-
- // 2. Delete secret from Infisical
- await axios.delete(
- `${BASE_URL}/api/v3/secrets/${secretKey}`,
- {
- workspaceId: serviceTokenData.workspace,
- environment: serviceTokenData.environment,
- type: secretType
- },
- {
- headers: {
- Authorization: `Bearer ${serviceToken}`
- },
- }
- );
-};
-
-deleteSecrets();
-```
-
-
-
-```Python
-import requests
-
-BASE_URL = "https://app.infisical.com"
-
-
-def delete_secrets():
- service_token = ""
- secret_type = "shared" # "shared" or "personal"
- secret_key = "some_key"
-
- # 1. Get your Infisical Token data
- service_token_data = requests.get(
- f"{BASE_URL}/api/v2/service-token",
- headers={"Authorization": f"Bearer {service_token}"},
- ).json()
-
- # 2. Delete secret from Infisical
- requests.delete(
- f"{BASE_URL}/api/v2/secrets/{secret_key}",
- json={
- "workspaceId": service_token_data["workspace"],
- "environment": service_token_data["environment"],
- "type": secret_type
- },
- headers={"Authorization": f"Bearer {service_token}"},
- )
-
-
-delete_secrets()
-
-```
-
-
-
- If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header.
-
-
-
-
\ No newline at end of file
diff --git a/docs/api-reference/overview/examples/integration.mdx b/docs/api-reference/overview/examples/integration.mdx
new file mode 100644
index 000000000..71f5b6de4
--- /dev/null
+++ b/docs/api-reference/overview/examples/integration.mdx
@@ -0,0 +1,90 @@
+---
+title: "Configure native integrations via API"
+description: "How to use Infisical API to sync secrets to external secret managers"
+---
+
+The Infisical API allows you to create programmatic integrations that connect with third-party secret managers to synchronize secrets from Infisical.
+
+This guide will primarily demonstrate the process using AWS Secret Store Manager (AWS SSM), but the steps are generally applicable to other secret management integrations.
+
+
+ For details on setting up AWS SSM synchronization and understanding its prerequisites, refer to the [AWS SSM integration setup documentation](../../../integrations/cloud/aws-secret-manager).
+
+
+
+
+ Authentication is required for all integrations. Use the [Integration Auth API](../../endpoints/integrations/create-auth) with the following parameters to authenticate.
+
+
+ Set this parameter to **aws-secret-manager**.
+
+
+ The Infisical project ID for the integration.
+
+
+ The AWS IAM User Access ID.
+
+
+ The AWS IAM User Access Secret Key.
+
+
+ ```bash Request
+ curl --request POST \
+ --url https://app.infisical.com/api/v1/integration-auth/access-token \
+ --header 'Authorization: ' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "workspaceId": "",
+ "integration": "aws-secret-manager",
+ "accessId": "",
+ "accessToken": ""
+ }'
+ ```
+
+
+
+ Once authentication between AWS SSM and Infisical is established, you can configure the synchronization behavior.
+ This involves specifying the source (environment and secret path in Infisical) and the destination in SSM to which the secrets will be synchronized.
+
+ Use the [integration API](../../endpoints/integrations/create) with the following parameters to configure the sync source and destination.
+
+
+ The ID of the integration authentication object used with AWS, obtained from the previous API response.
+
+
+ Indicates whether the integration should be active or inactive.
+
+
+ The secret name for saving in AWS SSM, which can be arbitrarily chosen.
+
+
+ The AWS region where the SSM is located, e.g., `us-east-1`.
+
+
+ The Infisical environment slug from which secrets will be synchronized, e.g., `dev`.
+
+
+ The Infisical folder path from which secrets will be synchronized, e.g., `/some/path`. The root path is `/`.
+
+
+ ```bash Request
+ curl --request POST \
+ --url https://app.infisical.com/api/v1/integration \
+ --header 'Authorization: ' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "integrationAuthId": "",
+ "sourceEnvironment": "",
+ "secretPath": "",
+ "app": "",
+ "region": ""
+ }'
+ ```
+
+
+
+
+
+Congratulations! You have successfully set up an integration to synchronize secrets from Infisical with AWS SSM.
+For more information, [view the integration API reference](../../endpoints/integrations).
+
\ No newline at end of file
diff --git a/docs/api-reference/overview/examples/note.mdx b/docs/api-reference/overview/examples/note.mdx
deleted file mode 100644
index 8491dfaae..000000000
--- a/docs/api-reference/overview/examples/note.mdx
+++ /dev/null
@@ -1,54 +0,0 @@
----
-title: "Note on E2EE"
----
-
-Each project in Infisical can have **End-to-End Encryption (E2EE)** enabled or disabled.
-
-By default, all projects have **E2EE** enabled which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side; this can be (optionally) disabled. However, this has limitations around functionality and ease-of-use:
-
-- You cannot make HTTP calls to Infisical to read/write secrets in plaintext.
-- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation.
-
-
-
- Example read/write secrets without client-side encryption/decryption
-
-
- Example read/write secrets with client-side encryption/decryption
-
-
-
-## FAQ
-
-
-
- We recommend starting with having **E2EE** enabled and disabling it if:
-
- - You're self-hosting Infisical, so having your instance of Infisical be able to read your secrets isn't an issue.
- - You want an easier way to read/write secrets with Infisical.
- - You need more power out of non-E2EE features such as secret rotation, dynamic secrets, etc.
-
-
-
- You can enable/disable E2EE for your project in Infisical in the Project Settings.
-
-
- It is secure and in fact how most vendors in our industry are able to offer features like secret rotation. In this mode, secrets are encrypted at rest by
- a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server.
-
- If you're concerned about Infisical Cloud's ability to read your secrets, then you may wish to
- use it with **E2EE** enabled or self-host Infisical on your own infrastructure and disable E2EE there.
-
- As an organization, we do not read any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization.
-
-
\ No newline at end of file
diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx
index 06ee491b5..6d577e15b 100644
--- a/docs/api-reference/overview/introduction.mdx
+++ b/docs/api-reference/overview/introduction.mdx
@@ -1,5 +1,6 @@
---
-title: "Introduction"
+title: "API Reference"
+sidebarTitle: "Introduction"
---
Infisical's Public (REST) API provides users an alternative way to programmatically access and manage
diff --git a/docs/cli/commands/export.mdx b/docs/cli/commands/export.mdx
index 49446e734..91ed215ef 100644
--- a/docs/cli/commands/export.mdx
+++ b/docs/cli/commands/export.mdx
@@ -33,6 +33,9 @@ Export environment variables from the platform into a file format.
# Export variables to a YAML file
infisical export --format=yaml > secrets.yaml
+
+ # Render secrets using a custom template file
+ infisical export --template=
```
### Environment variables
@@ -57,6 +60,26 @@ Export environment variables from the platform into a file format.
### flags
+
+ The `--template` flag specifies the path to the template file used for rendering secrets. When using templates, you can omit the other format flags.
+
+ ```text my-template-file
+ {{$secrets := secret "" "" ""}}
+ {{$length := len $secrets}}
+ {{- "{"}}
+ {{- with $secrets }}
+ {{- range $index, $secret := . }}
+ "{{ $secret.Key }}": "{{ $secret.Value }}"{{if lt $index (minus $length 1)}},{{end}}
+ {{- end }}
+ {{- end }}
+ {{ "}" -}}
+ ```
+
+ ```bash
+ # Example
+ infisical export --template="/path/to/template/file"
+ ```
+
Used to set the environment that secrets are pulled from.
diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx
index 3028ec110..2758ced00 100644
--- a/docs/cli/commands/login.mdx
+++ b/docs/cli/commands/login.mdx
@@ -12,4 +12,53 @@ The CLI uses authentication to verify your identity. When you enter the correct
To change where the login credentials are stored, visit the [vaults command](./vault).
-If you have added multiple users, you can switch between the users by using the [user command](./user).
\ No newline at end of file
+If you have added multiple users, you can switch between the users by using the [user command](./user).
+
+
+### Flags
+
+ ```bash
+ infisical login --method= # Optional, will default to 'user'.
+ ```
+
+ #### Valid values for the `method` flag are:
+ - `user`: Login using email and password.
+ - `universal-auth`: Login using a universal auth client ID and client secret.
+
+
+ When `method` is set to `universal-auth`, the `client-id` and `client-secret` flags are required. Optionally you can set the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` and `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variables instead of using the flags.
+
+ When you authenticate with universal auth, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable.
+
+ Use flag `--plain` along with `--silent` to print only the token in plain text when using the `universal-auth` method.
+
+
+
+
+
+ ```bash
+ infisical login --client-id= # Optional, required if --method=universal-auth.
+ ```
+
+ #### Description
+ The client ID of the universal auth client. This is required if the `--method` flag is set to `universal-auth`.
+
+
+ The `client-id` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` environment variable.
+
+
+
+ ```bash
+ infisical login --client-secret= # Optional, required if --method=universal-auth.
+ ```
+ #### Description
+ The client secret of the universal auth client. This is required if the `--method` flag is set to `universal-auth`.
+
+
+ The `client-secret` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variable.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/cli/commands/token.mdx b/docs/cli/commands/token.mdx
new file mode 100644
index 000000000..5b0d4ad5c
--- /dev/null
+++ b/docs/cli/commands/token.mdx
@@ -0,0 +1,21 @@
+---
+title: "infisical token"
+description: "Manage your Infisical identity access tokens"
+---
+
+```bash
+infisical service-token renew
+```
+
+## Description
+The Infisical `token` command allows you to manage your universal auth access tokens.
+With this command, you can renew your access tokens. In the future more subcommands will be added to better help you manage your tokens through the CLI.
+
+
+ Use this command to renew your access token. This command will renew your access token and output a renewed access token to the console.
+
+ ```bash
+ $ infisical token renew
+ ```
+
+
diff --git a/docs/documentation/getting-started/introduction-new.mdx b/docs/documentation/getting-started/introduction-new.mdx
new file mode 100644
index 000000000..c8eee8739
--- /dev/null
+++ b/docs/documentation/getting-started/introduction-new.mdx
@@ -0,0 +1,107 @@
+---
+mode: 'custom'
+---
+
+export function openSearch() {
+ document.getElementById('search-bar-entry').click();
+}
+
+
+
+
+
+
+
+ Infisical Documentation
+
+
+ What can we help you build?
+
+
+
+ Start a chat with us...
+
+
+
+
+
+
+
+ Choose a topic below or simply{' '}
+ get started
+
+
+
+
+ Practical guides and best practices to get you up and running quickly.
+
+
+ Comprehensive details about the Infisical API.
+
+
+ Learn more about Infisical's architecture and underlying security.
+
+
+ Read self-hosting instruction for Infisical.
+
+
+ Infisical's growing number of third-party integrations.
+
+
+ News about features and changes in Pinecone and related tools.
+
+
+
+
\ No newline at end of file
diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx
index 4c96b5a78..0f414c62a 100644
--- a/docs/documentation/getting-started/introduction.mdx
+++ b/docs/documentation/getting-started/introduction.mdx
@@ -1,107 +1,97 @@
---
-title: "Introduction"
+title: "What is Infisical?"
+sidebarTitle: "What is Infisical?"
+description: "An Introduction to the Infisical secret management platform."
---
-Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secrets management platform for storing, managing, and syncing
-application configuration and secrets like API keys, database credentials, and environment variables across applications and infrastructure.
+Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers.
+It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database
+credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure
+sharing of secrets among engineers.
-Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
-
-## Learn about Infisical
-
-
- Store secrets like API keys, database credentials, environment variables with Infisical
-
-
-## Access secrets
+Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
-
- Inject secrets into any application process/environment
+
+ Get started with Infisical Cloud in just a few minutes.
+
+
+ Self-host Infisical on your own infrastructure.
+
+
+
+## Why Infisical?
+
+Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical:
+- Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines).
+- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project).
+- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments.
+- Secure and compliant secret management practices in **[production environments](/sdks/overview)**.
+- **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more.
+- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities.
+
+## How does Infisical work?
+
+To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below.
+
+**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**.
+
+As a result, the 3 main concepts that are important to understand are:
+- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them.
+- **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)).
+- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, etc.).
+
+## How to get started with Infisical?
+
+Depending on your use case, it might be helpful to look into some of the resources and guides provided below.
+
+
+
+ Inject secrets into any application process/environment.
- Fetch secrets with any programming language on demand
+ Fetch secrets with any programming language on demand.
-
- Inject secrets into Docker containers
+
+ Inject secrets into Docker containers.
- Fetch and save secrets as native Kubernetes secrets
+ Fetch and save secrets as native Kubernetes secrets.
- Fetch secrets via HTTP request
-
-
-
-## Resources
-
-
-
- Learn how to configure and deploy Infisical
-
-
- Explore guides for every language and stack
+ Fetch secrets via HTTP request.
- Explore integrations for GitHub, Vercel, Netlify, and more
-
-
- Explore integrations for Next.js, Express, Django, and more
-
-
- Scan and prevent 140+ secret type leaks in your codebase
-
-
- Questions? Need help setting up? Book a 1x1 meeting with us
+ Explore integrations for GitHub, Vercel, AWS, and more.
diff --git a/docs/documentation/getting-started/platform.mdx b/docs/documentation/getting-started/platform.mdx
index 429524161..1a1164a40 100644
--- a/docs/documentation/getting-started/platform.mdx
+++ b/docs/documentation/getting-started/platform.mdx
@@ -21,7 +21,7 @@ Here, you can also create a new project.
The **Members** page lets you add or remove external members to your organization.
Note that you can configure your organization in Infisical to have members authenticate with the platform via protocols like SAML 2.0.
-
+
## Managing your Projects
diff --git a/docs/documentation/getting-started/sdks.mdx b/docs/documentation/getting-started/sdks.mdx
index aef15294a..b3e8a3925 100644
--- a/docs/documentation/getting-started/sdks.mdx
+++ b/docs/documentation/getting-started/sdks.mdx
@@ -18,4 +18,4 @@ Follow the instructions for your language use the SDK for it:
- [Java SDK](https://infisical.com/docs/sdks/languages/java)
- [.NET SDK](https://infisical.com/docs/sdks/languages/csharp)
-Missing a language? [Throw in a request](https://github.com/Infisical/infisical/issues).
\ No newline at end of file
+Missing a language? [Throw in a request here](https://github.com/Infisical/infisical/issues).
diff --git a/docs/documentation/guides/local-development.mdx b/docs/documentation/guides/local-development.mdx
new file mode 100644
index 000000000..c2651cb58
--- /dev/null
+++ b/docs/documentation/guides/local-development.mdx
@@ -0,0 +1,34 @@
+---
+title: "Secret Management in Development Environments"
+sidebarTitle: "Local Development"
+description: "Learn how to manage secrets in local development environments."
+---
+
+## Problem at hand
+
+There is a number of issues that arise with secret management in local development environment:
+1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another.
+2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time.
+3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability.
+
+## Solution
+
+One of the main benefits of Infisical is the facilitation of secret management workflows in local development use cases. In particular, Infisical heavily follows the "Security Shift Left" principle to enable developers to effotlessly follow secure practices when coding.
+
+### CLI
+
+[Infisical CLI](/cli/overview) is the most frequently used Infisical tool for secret management in local development environments. It makes it easy to inject secrets right into the local application environments based on the permissions given to corresponsing developers.
+
+### Dashboard
+
+On top of that, Infisical provides a great [Web Dashboard](https://app.infisical.com/signup) that can be used to making quick secret updates.
+
+
+
+### Personal Overrides
+
+By default, all the secrets in the Infisical environments are shared among project members who have the permission to access those environment. At the same time, when doing local development, it is often desirable to change the value of a certain secret only for a particular self. For such use cases, Infisical supports the functionality of **Personal Overrides** โ which allow developers to override values of any secrets without affecting the workflows of the rest of the team. Personal Overrides can be created both in the dashboard or via [Infisical CLI](/cli/overview).
+
+### Secret Scanning
+
+In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionlality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub.
\ No newline at end of file
diff --git a/docs/documentation/guides/microsoft-power-apps.mdx b/docs/documentation/guides/microsoft-power-apps.mdx
new file mode 100644
index 000000000..64647c8e6
--- /dev/null
+++ b/docs/documentation/guides/microsoft-power-apps.mdx
@@ -0,0 +1,114 @@
+---
+title: "Microsoft Power Apps"
+description: "Learn how to manage secrets in Microsoft Power Apps with Infisical."
+---
+In recent years, there has been a shift towards so-called low-code and no-code platforms. These platforms are particularly appealing to businesses without internal development capabilities, yet teams often discover that some coding is necessary to fully satisfy their business needs.
+
+Low-code platforms have become increasingly sophisticated and useful, leading to a rise in their adoption by businesses. A prime example is Microsoft Power Apps, which offers a range of data sources and service integrations right out of the box. However, even with advanced tools, you might not always find a ready-made solution for every challenge. This means that low-code doesn't equate to no-code, as some coding and customization are still required to cater to specific needs.
+
+Consider the need for data integrations where an HTTP-based call to a web service might be necessary, typically requiring authentication through an API key or another type of secret.
+
+Importantly, it's crucial to avoid hardcoding these secrets, as they would then be accessible to anyone with collaboration rights to the code. This underscores the importance of using a secret management solution like Infisical.
+
+In this article, we'll demonstrate how to retrieve app secrets from Infisical for use in a Power Apps application. We'll create a simple application with a dedicated data connector to illustrate the ease of integrating Infisical with Power Apps. This tutorial assumes some prior programming experience in C#.
+
+Prerequisites:
+- Created Microsoft Power App.
+
+
+
+ First, letโs create a new Azure Function using the Azure Management Portal. Get the [Function App](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/Microsoft.FunctionApp?tab=Overview) from the [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/).
+ 
+
+ Place it in a subscription using any resource group. The name of the function is arbitrary. We'll use .NET as a runtime stack, but you can use whatever you're most comfortable with. The OS choice is also up to you. While Linux may look like a lightweight solution, Windows actually has more Azure Functions support. For instance, you cannot edit a Linux-based Azure Function within the Azure management portal.
+
+ By using a consumption plan, we'll only pay for the resources we use when they are requested. This is the classic โserverlessโ approach, where you do not pay for running servers, only for interactivity.
+
+ Once the new Azure Functions instance is ready, we add a function. In this case, we can do that already from the Azure Management Portal. Use the โHTTP triggerโ template and choose the โfunctionโ authorization level.
+
+ The code for the first function can be as simple as:
+
+ ```
+ using System.Net;
+
+ public static async Task Run(HttpRequestMessage req, TraceWriter log)
+ {
+ log.Info("C# HTTP trigger function processed a request.");
+ return req.CreateResponse(HttpStatusCode.OK, "Hello World");
+ }
+ ```
+
+
+ The code above is written for the older runtime. As a result, you may need to change the runtime version to 1 for the Azure Power Apps integration to work. If we start at a newer version (for example, 3) this triggers a warning before the migration.
+
+
+ Finally, we also need to publish the Swagger (or API) definitions and enable cross-origin resource sharing (CORS). While the API definitions are rather easy to set up, the correct CORS value may be tricky. For now, we can use the wildcard option to allow all hosts.
+
+
+
+
+ Once we set all this up, itโs time to create the custom connector.
+
+ You can create the custom connector via the data pane. When we use โCreate from Azure Service (Preview)โ, this yields a dialog similar to the following:
+
+ 
+
+ We can now fill out the fields using the information for our created function. The combination boxes are automatically filled in order. Once we select one of the reachable subscriptions (tied to the same account weโve used to log in to create a Power App), the available services are displayed. Once we select our Azure Functions service, we select the function for retrieving the secret.
+
+
+
+
+ You can add Infisical in an Azure Function quite easily using the [Infisical SDK for .NET](https://infisical.com/docs/sdks/languages/csharp) (or other languages). This enables the function to communicate with Infisical to obtain secrets, among other things.
+
+ In short, we can simply bring all the necessary classes over and start using the Client class. Essentially, this enables us to write code like this:
+
+ ```
+ var settings = new ClientSettings
+ {
+ ClientId = "CLIENT_ID",
+ ClientSecret = "CLIENT_SECRET",
+ // SiteUrl = "http://localhost:8080", <-- This line can be omitted if you're using Infisical Cloud.
+ };
+ var infisical = new InfisicalClient(settings);
+
+ var options = new GetSecretOptions
+ {
+ SecretName = "TEST",
+ ProjectId = "PROJECT_ID",
+ Environment = "dev",
+ };
+ var secret = infisical.GetSecret(options);
+ ```
+
+ Knowing the URL of Infisical as well as the Client Id and Client Secret, we can now access the desired values.
+
+ Now itโs time to actually use the secret within a Power App. There are two ways to request a desired target service with a secret retrieved from the function:
+
+ 1. Call the function first, retrieve the secret, then call the target service, for example, via another custom connector with the secret as input.
+
+ 2. Perform the final API request within the function call โ not returning a secret at all, just the response from invoking the target service.
+
+ While the first option is more flexible (and presumably cheaper!), the second option is definitely easier. In the end, you should mostly decide based on whether the function should be reused for other purposes. If the single Power App is the only consumer of the function, it may make more sense to go with the second option. Otherwise, you should use the first option.
+
+ For our simple example, we donโt need to reuse the function. We also donโt want the additional complexity of maintaining two different custom connectors, where we only use one to pass data to the other one.
+
+ Based on the previous snippet, we create the following code (for proxying a GET request from an API accessible via the URL specified in the apiEndpoint variable).
+
+ ```
+ using (var client = new HttpClient())
+ {
+ client.DefaultRequestHeaders
+ .Accept
+ .Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ client.DefaultRequestHeaders.Add("X-API-KEY", secret);
+
+ var result = await client.GetAsync(apiEndpoint);
+ var resultContent = await result.Content.ReadAsStringAsync();
+ req.CreateResponse(HttpStatusCode.OK, resultContent);
+ }
+ ```
+ This creates a request to the resource protected by an API key that is retrieved from Infisical.
+
+
+
\ No newline at end of file
diff --git a/docs/documentation/guides/nextjs-vercel.mdx b/docs/documentation/guides/nextjs-vercel.mdx
index 2e8805cd0..5aeadc752 100644
--- a/docs/documentation/guides/nextjs-vercel.mdx
+++ b/docs/documentation/guides/nextjs-vercel.mdx
@@ -193,7 +193,7 @@ Next, navigate to your project's integrations tab in Infisical and press on the

-
+
Opting in for the Infisical-Vercel integration will break end-to-end encryption since Infisical will be able to read
@@ -205,8 +205,8 @@ Next, navigate to your project's integrations tab in Infisical and press on the
Now select **Production** for (the source) **Environment** and sync it to the **Production Environment** of the (target) application in Vercel.
Lastly, press create integration to start syncing secrets to Vercel.
-
-
+
+
You should now see your secret from Infisical appear as production environment variables in your Vercel project.
diff --git a/docs/documentation/platform/access-controls/access-requests.mdx b/docs/documentation/platform/access-controls/access-requests.mdx
new file mode 100644
index 000000000..45c155ab4
--- /dev/null
+++ b/docs/documentation/platform/access-controls/access-requests.mdx
@@ -0,0 +1,22 @@
+---
+title: "Access Requests"
+description: "Learn how to request access to sensitive resources in Infisical."
+---
+
+In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
+
+This functionality works in the following way:
+1. A project administrator sets up a policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
+
+
+
+2. When a developer requests access to one of such sensitive resources, the request is visible in the dashboard, and the corresponding eligible approvers get an email notification about it.
+
+
+
+3. An eligible approver can approve or reject the access request.
+
+
+4. As soon as the request is approved, developer is able to access the sought resources.
+
+
diff --git a/docs/documentation/platform/access-controls/additional-privileges.mdx b/docs/documentation/platform/access-controls/additional-privileges.mdx
new file mode 100644
index 000000000..8f29d1d6e
--- /dev/null
+++ b/docs/documentation/platform/access-controls/additional-privileges.mdx
@@ -0,0 +1,22 @@
+---
+title: "Additional Privileges"
+description: "Learn how to add specific privileges on top of predefined roles."
+---
+
+Even though Infisical supports full-fledged [role-base access controls](./role-based-access-controls) with ability to set predefined permissions for user and machine identities, it is sometimes desired to set additional privileges for specific user or machine identities on top of their roles.
+
+Infisical **Additional Privileges** functionality enables specific permissions with access to sensitive secrets/folders by identities within certain projects. It is possible to set up additional privileges through Web UI or API.
+
+To provision specific privileges through Web UI:
+1. Click on the `Edit` button next to the set of roles for user or identities.
+
+
+2. Click `Add Additional Privileges` in the corresponding section of the permission management modal.
+
+
+3. Fill out the necessary parameters in the privilege entry that appears. It is possible to specify the `Environment` and `Secret Path` to which you want to enable access.
+It is also possible to define the range of permissions (`View`, `Create`, `Modify`, `Delete`) as well as how long the access should last (e.g., permanent or timed).
+
+
+4. Click the `Save` button to enable the additional privilege.
+
\ No newline at end of file
diff --git a/docs/documentation/platform/access-controls/overview.mdx b/docs/documentation/platform/access-controls/overview.mdx
new file mode 100644
index 000000000..54fc8ff25
--- /dev/null
+++ b/docs/documentation/platform/access-controls/overview.mdx
@@ -0,0 +1,58 @@
+---
+title: "Access Controls"
+sidebarTitle: "Overview"
+description: "Learn about Infisical's access control toolset."
+---
+
+To make sure that users and machine identities are only accessing the resources and performing actions they are authorized to, Infisical supports a wide range of access control tools.
+
+
+
+ Manage user and machine identitity permissions through predefined roles.
+
+
+ Add specific privileges to users and machines on top of their roles.
+
+
+ Grant timed access to roles and specific privileges.
+
+
+ Enable users to request (temporary) access to sensitive resources.
+
+
+ Set up review policies for secret changes in sensitive environments.
+
+
+ Track every action performed by user and machine identities in Infisical.
+
+
diff --git a/docs/documentation/platform/access-controls/role-based-access-controls.mdx b/docs/documentation/platform/access-controls/role-based-access-controls.mdx
new file mode 100644
index 000000000..98a2e4659
--- /dev/null
+++ b/docs/documentation/platform/access-controls/role-based-access-controls.mdx
@@ -0,0 +1,44 @@
+---
+title: "Role-based Access Controls"
+description: "Learn how to use RBAC to manage user permissions."
+---
+
+Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles male it possible to restrict access to resources and the range of actions that can be performed.
+
+In general, access controls can be split up across [projects](/documentation/platform/project) and [organizations](/documentation/platform/organization).
+
+## Organization-level access controls
+
+By default, every user and machine identity in a organization is either an **admin** or a **member**.
+
+**Admins** are able to perform every action with the organization, including adding and removing organization members, managing access controls, setting up security settings, and creating new projects.
+
+**Members**, on the other hand, are restricted from removing organization members, modifying billing information, updating access controls, and performing a number of other actions.
+
+Overall, organization-level access controls are significantly of administrative nature. Access to projects, secrets and other sensitive data is specified on the project level.
+
+
+
+## Project-level access controls
+
+By default, every user in a project is either a **viewer**, **developer**, or an **admin**. Each of these roles comes with a varying access to different features and resources inside projects.
+
+As such:
+- **Admin**: This role enables identities to have access to all environments, folders, secrets, and actions within the project.
+- **Developers**: This role restricts identities from performing project control actions, updating Approval Workflow policies, managing roles/members, and more.
+- **Viewer**: The most limiting bulit-in role on the project level โย it forbids user and machine identities to perform any action and rather shows them in the read-only mode.
+
+
+
+## Creating custom roles
+
+By creating custom roles, you are able to adjust permissions to the needs of your organization. This can be useful for:
+- Creating superadmin roles, roles specific to SRE engineers, etc.
+- Restricting access of users to specific secrets, folders, and environments.
+- Embedding these specific roles into [Approval Workflow policies](/documentation/platform/pr-workflows).
+
+
+It is worth noting that users are able to assume multiple built-in and custom roles. A user will gain access to all actions within the roles assigned to them, not just the actions those roles share in common.
+
+
+
diff --git a/docs/documentation/platform/access-controls/temporary-access.mdx b/docs/documentation/platform/access-controls/temporary-access.mdx
new file mode 100644
index 000000000..c914c96f5
--- /dev/null
+++ b/docs/documentation/platform/access-controls/temporary-access.mdx
@@ -0,0 +1,26 @@
+---
+title: "Temporary Access"
+description: "Learn how to set up timed access to sensitive resources for user and machine identities."
+---
+
+Certain environments and secrets are so sensitive that it is recommended to not give any user permanent access to those. For such use cases, Infisical supports the functionality of **Temporary Access** provisioning.
+
+
+To provision temporary access through Web UI:
+1. Click on the `Edit` button next to the set of roles for user or identities.
+
+
+2. Click `Permanent` next to the role or specific privilege that you want to make temporary.
+
+3. Specify the duration of remporary access (e.g., `1m`, `2h`, `3d`).
+
+
+4. Click `Grant`.
+
+5. Click the corresponding `Save` button to enable remporary access.
+
+
+
+Every user and machine identity should always have at least one permanent role attached to it.
+
+
diff --git a/docs/documentation/platform/audit-logs.mdx b/docs/documentation/platform/audit-logs.mdx
index b5a47df50..be2381da2 100644
--- a/docs/documentation/platform/audit-logs.mdx
+++ b/docs/documentation/platform/audit-logs.mdx
@@ -1,27 +1,28 @@
---
title: "Audit Logs"
-description: "See which events are triggered within your Infisical project."
+description: "Track evert event action performed within Infisical projects."
---
Note that Audit Logs is a paid feature.
- If you're using Infisical Cloud, then it is available under the **Team Tier**, **Pro Tier**,
+ If you're using Infisical Cloud, then it is available under the **Pro**,
and **Enterprise Tier** with varying retention periods. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
Infisical provides audit logs for security and compliance teams to monitor information access.
-With this feature, teams can track 25+ different events;
-filter audit logs by event, actor, source, date or any combination of these filters;
-and inspect extensive metadata in the event of any suspicious activity or incident review.
+With the Audit Log functionality, teams can:
+- **Track** 40+ different events;
+- **Filter** audit logs by event, actor, source, date or any combination of these filters;
+- **Inspect** extensive metadata in the event of any suspicious activity or incident review.

Each log contains the following data:
-- Event: The underlying action such as create, list, read, update, or delete secret(s).
-- Actor: The entity responsible for performing or causing the event; this can be a user or service.
-- Timestamp: The date and time at which point the event occurred.
-- Source (User agent + IP): The software (user agent) and network address (IP) from which the event was initiated.
-- Metadata: Additional data to provide context for each event. For example, this could be the path at which a secret was fetched from etc.
+- **Event**: The underlying action such as create, list, read, update, or delete secret(s).
+- **Actor**: The entity responsible for performing or causing the event; this can be a user or service.
+- **Timestamp**: The date and time at which point the event occurred.
+- **Source** (User agent + IP): The software (user agent) and network address (IP) from which the event was initiated.
+- **Metadata**: Additional data to provide context for each event. For example, this could be the path at which a secret was fetched from etc.
diff --git a/docs/documentation/platform/auth-methods/email-password.mdx b/docs/documentation/platform/auth-methods/email-password.mdx
new file mode 100644
index 000000000..db23026b8
--- /dev/null
+++ b/docs/documentation/platform/auth-methods/email-password.mdx
@@ -0,0 +1,14 @@
+---
+title: "Email and Password"
+description: "Learn how to authenticate into Infisical with email and password."
+---
+
+**Email and Password** is the most common authentication method that can be used by user identities for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it.
+
+It is currently possible to use the **Email and Password** auth method to authenticate into the Web Dashboard and Infisical CLI.
+
+Every **Email and Password** is accompanied by an emergency kit given to users during signup. If the password is lost or forgotten, emergency kit is only way to retrieve the access to your account. It is possible to generate a new emergency kit with the following steps:
+1. Open the `Personal Settings` menu.
+
+2. Scroll down to the `Emergency Kit` section.
+3. Enter your current password and click `Save`.
diff --git a/docs/documentation/platform/dynamic-secrets/mysql.mdx b/docs/documentation/platform/dynamic-secrets/mysql.mdx
new file mode 100644
index 000000000..c64edab63
--- /dev/null
+++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx
@@ -0,0 +1,115 @@
+---
+title: "MySQL"
+description: "Learn how to dynamically generate MySQL Database user passwords."
+---
+
+The Infisical MySQL dynamic secret allows you to generate MySQL Database credentials on demand based on configured role.
+
+## Prerequisite
+Create a user with the required permission in your SQL instance. This user will be used to create new accounts on-demand.
+
+
+## Set up Dynamic Secrets with MySQL
+
+
+
+ Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
+
+
+ 
+
+
+ 
+
+
+
+ Name by which you want the secret to be referenced
+
+
+
+ Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
+
+
+
+ Maximum time-to-live for a generated secret
+
+
+
+ Choose the service you want to generate dynamic secrets for. This must be selected as **MySQL**.
+
+
+
+ Database host
+
+
+
+ Database port
+
+
+
+ Username that will be used to create dynamic secrets
+
+
+
+ Password that will be used to create dynamic secrets
+
+
+
+ Name of the database for which you want to create dynamic secrets
+
+
+
+ A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
+
+
+
+
+ If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s).
+
+ 
+
+
+ After submitting the form, you will see a dynamic secret created in the dashboard.
+
+
+ If this step fails, you may have to add the CA certificate.
+
+
+ 
+
+
+ Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
+ To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
+ Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
+
+ 
+ 
+
+ When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
+
+ 
+
+
+ Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
+
+
+
+ Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
+
+ 
+
+
+
+## Audit or Revoke Leases
+Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
+This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
+
+
+
+## Renew Leases
+To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
+
+
+
+ Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
+
\ No newline at end of file
diff --git a/docs/documentation/platform/dynamic-secrets/oracle.mdx b/docs/documentation/platform/dynamic-secrets/oracle.mdx
new file mode 100644
index 000000000..05b832c4f
--- /dev/null
+++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx
@@ -0,0 +1,115 @@
+---
+title: "Oracle"
+description: "Learn how to dynamically generate Oracle Database user passwords."
+---
+
+The Infisical Oracle dynamic secret allows you to generate Oracle Database credentials on demand based on configured role.
+
+## Prerequisite
+Create a user with the required permission in your SQL instance. This user will be used to create new accounts on-demand.
+
+
+## Set up Dynamic Secrets with Oracle
+
+
+
+ Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
+
+
+ 
+
+
+ 
+
+
+
+ Name by which you want the secret to be referenced
+
+
+
+ Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
+
+
+
+ Maximum time-to-live for a generated secret
+
+
+
+ Choose the service you want to generate dynamic secrets for. This must be selected as **Oracle**.
+
+
+
+ Database host
+
+
+
+ Database port
+
+
+
+ Username that will be used to create dynamic secrets
+
+
+
+ Password that will be used to create dynamic secrets
+
+
+
+ Name of the database for which you want to create dynamic secrets
+
+
+
+ A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
+
+
+ 
+
+
+
+ If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s).
+
+
+ After submitting the form, you will see a dynamic secret created in the dashboard.
+
+
+ If this step fails, you may have to add the CA certficate.
+
+
+ 
+
+
+ Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
+ To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
+ Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
+
+ 
+ 
+
+ When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
+
+ 
+
+
+ Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
+
+
+
+ Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
+
+ 
+
+
+
+## Audit or Revoke Leases
+Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
+This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
+
+
+
+## Renew Leases
+To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
+
+
+
+ Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
+
\ No newline at end of file
diff --git a/docs/documentation/platform/dynamic-secrets/overview.mdx b/docs/documentation/platform/dynamic-secrets/overview.mdx
new file mode 100644
index 000000000..42bc33223
--- /dev/null
+++ b/docs/documentation/platform/dynamic-secrets/overview.mdx
@@ -0,0 +1,30 @@
+---
+title: "Overview"
+description: "Learn how to generate secrets dynamically on-demand."
+---
+
+## Introduction
+
+Contrary to static key-value secrets, which require manual input of data into the secure Infisical storage, dynamic secrets are generated on-demand upon access.
+
+Dynamic secrets are unique to every identity using them. Such secrets come are generated only at the moment they are retrieved, eliminating the possibility of theft or reuse by another identity. Thanks to Infisical's integrated revocation capabilities, dynamic secrets can be promptly invalidated post-use, significantly reducing their lifespan.
+
+## Benefits of Dynamic Secrets
+
+This approach offers several advantages in terms of security and management:
+
+- **Enhanced Security**: By frequently changing secrets, dynamic secrets minimize the risk associated with secret compromise. Even if an attacker manages to obtain a secret, it would likely be invalid by the time they attempt to use it.
+
+- **Reduced Secret Lifetime**: The limited validity period of dynamic secrets means that they are less valuable targets for attackers. This inherently reduces the time window during which a secret can be exploited.
+
+- **Automated Management**: Dynamic secrets enable automated systems to handle the generation, distribution, revocation, and rotation of secrets without human intervention, thus reducing the risk of human error.
+
+- **Auditing and Traceability**: The generation of dynamic secrets can be tightly controlled and monitored. This allows for detailed auditing of who accessed what secret and when, improving overall security posture and compliance with regulatory standards.
+
+- **Scalability**: Dynamic secret management systems can scale more effectively to handle a large number of services and applications, as they automate much of the overhead associated with manual secret management.
+
+Dynamic secrets are particularly useful in environments with stringent security requirements, such as cloud environments, distributed systems, and microservices architectures, where they help to manage database credentials, API keys, service tokens, and other types of secrets.
+
+## Infisical Dynamic Secret Templates
+
+1. [PostgreSQL](./postgresql)
diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx
new file mode 100644
index 000000000..78ba87ee5
--- /dev/null
+++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx
@@ -0,0 +1,118 @@
+---
+title: "PostgreSQL"
+description: "How to dynamically generate PostgreSQL database users"
+---
+
+The Infisical PostgreSQL dynamic secret allows you to generate PostgreSQL database credentials on demand based on configured role.
+
+## Prerequisite
+
+Create a user with the required permission in your SQL instance. This user will be used to create new accounts on-demand.
+
+
+## Set up Dynamic Secrets with PostgreSQL
+
+
+
+ Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
+
+
+ 
+
+
+ 
+
+
+
+ Name by which you want the secret to be referenced
+
+
+
+ Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
+
+
+
+ Maximum time-to-live for a generated secret
+
+
+
+ Choose the service you want to generate dynamic secrets for. This must be selected as **PostgreSQL**.
+
+
+
+ Database host
+
+
+
+ Database port
+
+
+
+ Username that will be used to create dynamic secrets
+
+
+
+ Password that will be used to create dynamic secrets
+
+
+
+ Name of the database for which you want to create dynamic secrets
+
+
+
+ A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
+
+
+ 
+
+
+
+ If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s).
+
+ 
+
+
+ After submitting the form, you will see a dynamic secret created in the dashboard.
+
+
+ If this step fails, you may have to add the CA certficate.
+
+
+ 
+
+
+ Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
+ To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
+ Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
+
+ 
+ 
+
+ When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
+
+ 
+
+
+ Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
+
+
+
+ Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
+
+ 
+
+
+
+## Audit or Revoke Leases
+Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
+This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
+
+
+
+## Renew Leases
+To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
+
+
+
+ Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
+
\ No newline at end of file
diff --git a/docs/documentation/platform/folder.mdx b/docs/documentation/platform/folder.mdx
index 162cabfb9..a3636a2ea 100644
--- a/docs/documentation/platform/folder.mdx
+++ b/docs/documentation/platform/folder.mdx
@@ -1,11 +1,12 @@
---
title: "Folders"
-description: "Organize your secrets with folders"
+description: "Learn how to organize secrets with folders."
---
-Infisical's folder feature lets you store secrets at a specific folder; we also call this **path-based secret storage**.
-This is great for organizing secrets around hierarchies when multiple services, types of secrets, etc. are involved at great quantities.
-With folders that can go infinitely deep, you can mirror your application architecture (be it microservices or monorepos)
+Infisical Folders enable users to organize secrets using custom structures dependent on the intended use case (also known as **path-based secret storage**).
+
+It is great for organizing secrets around hierarchies with multiple services or types of secrets involved at large quantities.
+Infisical Folders can be infinitely nested to mirror your application architecture โย whether it's microservices, monorepos,
or any logical grouping that best suits your needs.
Consider the following structure for a microservice architecture:
@@ -25,9 +26,7 @@ In this example, we store environment variables for each microservice under each
We also store user-specific secrets for micro-service 1 under `/service1/users`. With this folder structure in place, your applications only need to specify a path like `/microservice1/envars` to fetch secrets from there.
By extending this example, you can see how path-based secret storage provides a versatile approach to manage secrets for any architecture.
-## Folders
-
-### Managing folders
+## Managing folders
To add a folder, press the downward chevron to the right of the **Add Secret** button; then press on the **Add Folder** button.
diff --git a/docs/documentation/platform/groups.mdx b/docs/documentation/platform/groups.mdx
new file mode 100644
index 000000000..18bfe6fa5
--- /dev/null
+++ b/docs/documentation/platform/groups.mdx
@@ -0,0 +1,67 @@
+---
+title: "User Groups"
+description: "Manage user groups in Infisical."
+---
+
+
+ User Groups is a paid feature.
+
+ If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
+ then you should contact team@infisical.com to purchase an enterprise license to use it.
+
+
+## Concept
+
+A (user) group is a collection of users that you can create in an Infisical organization to more efficiently manage permissions and access control for multiple users together. For example, you can have a group called `Developers` with the `Developer` role containing all the developers in your organization.
+
+User groups have the following properties:
+
+- If a group is added to a project under specific role(s), all users in the group will be provisioned access to the project with the role(s). Conversely, if a group is removed from a project, all users in the group will lose access to the project.
+- If a user is added to a group, they will inherit the access control properties of the group including access to project(s) under the role(s) assigned to the group. Conversely, if a user is removed from a group, they will lose access to project(s) that the group has access to.
+- If a user was previously added to a project under a role and is later added to a group that has access to the same project under a different role, then the user will now have access to the project under the composite permissions of the two roles. If the group is subsequently removed from the project, the user will not lose access to the project as they were previously added to the project separately.
+- A user can be part of multiple groups. If a user is part of multiple groups, they will inherit the composite permissions of all the groups that they are part of.
+
+## Workflow
+
+In the following steps, we explore how to create and use user groups to provision user access to projects in Infisical.
+
+
+
+ To create a group, head to your Organization Settings > Access Control > Groups and press **Create group**.
+
+ 
+
+ When creating a group, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
+
+ 
+
+ Now input a few details for your new group. Hereโs some guidance for each field:
+ - Name (required): A friendly name for the group like `Engineering`.
+ - Slug (required): A unique identifier for the group like `engineering`.
+ - Role (required): A role from the Organization Roles tab for the group to assume. The organization role assigned will determine what organization level resources this group can have access to.
+
+
+ Next, you'll want to assign users to the group. To do this, press on the users icon on the group and start assigning users to the group.
+
+ 
+
+ In this example, we're assigning **Alan Turing** and **Ada Lovelace** to the group **Engineering**.
+
+ 
+
+
+ To enable the group to access project-level resources such as secrets within a specific project, you should add it to that project.
+
+ To do this, head over to the project you want to add the group to and go to Project Settings > Access Control > Groups and press **Add group**.
+
+ 
+
+ Next, select the group you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this group can have access to.
+
+ 
+
+ That's it!
+
+ The users of the group now have access to the project under the role you assigned to the group.
+
+
\ No newline at end of file
diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx
new file mode 100644
index 000000000..77999effe
--- /dev/null
+++ b/docs/documentation/platform/identities/machine-identities.mdx
@@ -0,0 +1,59 @@
+---
+title: Machine Identities
+description: "Learn how to use Machine Identities to programmatically interact with Infisical."
+---
+
+## Concept
+
+An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP).
+
+Each identity must authenticate with the API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) to get back a short-lived access token to be used in subsequent requests.
+
+
+
+Key Features:
+
+- Role Assignment: Identities must be assigned [roles](/documentation/platform/role-based-access-controls). These roles determine the scope of access to resources, either at the organization level or project level.
+- Auth/Token Configuration: Identities must be configured with corresponding authentication methods and access token properties to securely interact with the Infisical API.
+
+## Workflow
+
+A typical workflow for using identities consists of four steps:
+
+1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
+This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth).
+2. Adding the identity to the project(s) you want it to have access to.
+3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
+4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
+
+
+
+ Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, Terraform, Kubernetes Operator, and Infisical Agent. They do not work with clients such as CLI, Ansible look up plugin, etc.
+
+ Machine Identity support for the rest of the clients is planned to be released in the current quarter.
+
+
+
+## Authentication Methods
+
+To interact with various resources in Infisical, Machine Identities are able to authenticate using:
+
+- [Universal Auth](/documentation/platform/identities/universal-auth): the most versatile authentication method that can be configured on an identity from any platform/environment to access Infisical.
+
+## FAQ
+
+
+
+ A service token is a project-level authentication method that is being phased out in favor of identities.
+
+ Amongst many differences, identities provide broader access over the Infisical API, utilizes the same
+ permission system as user identities, and come with a significantly larger number of configurable authentication and security features.
+
+
+ There are a few reasons for why this might happen:
+
+ - You have insufficient organization permissions to create, read, update, delete identities.
+ - The identity you are trying to read, update, or delete is more privileged than yourself.
+ - The role you are trying to create an identity for or update an identity to is more privileged than yours.
+
+
diff --git a/docs/documentation/platform/identities/overview.mdx b/docs/documentation/platform/identities/overview.mdx
index 7a4751487..18c173766 100644
--- a/docs/documentation/platform/identities/overview.mdx
+++ b/docs/documentation/platform/identities/overview.mdx
@@ -1,53 +1,26 @@
---
-title: Identities
-description: "Programmatically interact with Infisical"
+title: "User and Machine Identities"
+sidebarTitle: "Overview"
+description: "Learn more about identities to interact with resources in Infisical."
---
-
- Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, and Agent. They do not work with clients such as CLI, K8s Operator, Terraform Provider, etc.
+To interact with secrets and resource with Infisical, it is important to undrestand the concept of identities.
+Identities can be of two types:
+- **People** (e.g., developers, platform engineers, administrators)
+- **Machines** (e.g., machine entities for managing secrets in CI/CD pipelines, production applications, and more)
- We will be releasing compatibility with it across clients in the coming quarter.
-
+Both people and machines are able to utilize corresponding clients (e.g., Dashboard UI, CLI, SDKs, API, Kubernetes Operator) together with allowed authentication methods (e.g., email & password, SAML SSO, LDAP, OIDC, Universal Auth).
-## Concept
-
-A (machine) identity is an entity that you can create in an Infisical organization to represent a workload or application that requires access to the Infisical API. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP).
-
-Each identity must authenticate with the API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) to get back a short-lived access token to be used in subsequent requests.
-
-Key Features:
-
-- Role Assignment: Identities must be assigned [roles](/documentation/platform/role-based-access-controls). These roles determine the scope of access to resources, either at the organization level or project level.
-- Auth/Token Configuration: Identities must be configured with auth methods and access token properties to securely interact with the Infisical API.
-
-## Workflow
-
-A typical workflow for using identities consists of four steps:
-
-1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
-This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth).
-2. Adding the identity to the project(s) you want it to have access to.
-3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
-4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
-
-Check out the following authentication method-specific guides for step-by-step instruction on how to use identities to access Infisical:
-
-- [Universal Auth](/documentation/platform/identities/universal-auth)
-
-**FAQ**
-
-
-
- A service token is a project-level authentication method that is being phased out in favor of identities.
-
- Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based
- permission system used by users, and comes with ample more configurable authentication and security features.
-
-
- There are a few reasons for why this might happen:
-
- - You have insufficient organization permissions to create, read, update, delete identities.
- - The identity you are trying to read, update, or delete is more privileged than yourself.
- - The role you are trying to create an identity for or update an identity to is more privileged than yours.
-
-
+
+
+ Learn more about the concept on user identities in Infisical.
+
+
+ Understand the concept of machine identities in Infisical.
+
+
diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx
index e60ae1e33..a9f4dffae 100644
--- a/docs/documentation/platform/identities/universal-auth.mdx
+++ b/docs/documentation/platform/identities/universal-auth.mdx
@@ -1,9 +1,9 @@
---
title: Universal Auth
-description: "Authenticate with Infisical from any platform/environment"
+description: "Learn how to authenticate to Infisical from any platform or environment."
---
-**Universal Auth** is the most versatile authentication method that can be configured on an identity from any platform/environment to access Infisical.
+**Universal Auth** is the most versatile authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to access Infisical from any platform or environment.
In this method, each identity is given a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token to authenticate with the Infisical API.
@@ -50,7 +50,7 @@ using the Universal Auth authentication method.
Restricting **Client Secret** and access token usage to specific trusted IPs is a paid feature.
- If youโre using Infisical Cloud, then it is available under the Pro Tier. If youโre self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it.
+ If youโre using Infisical Cloud, then it is available under the Pro Tier. If youโre self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
diff --git a/docs/documentation/platform/identities/user-identities.mdx b/docs/documentation/platform/identities/user-identities.mdx
new file mode 100644
index 000000000..2d4791127
--- /dev/null
+++ b/docs/documentation/platform/identities/user-identities.mdx
@@ -0,0 +1,23 @@
+---
+title: User Identities
+description: "Read more about the concept of user identities in Infisical."
+---
+
+## Concept
+
+A **user identity** (also known as **user**) represents a developer, admin, or any other human entity interacting with resources in Infisical.
+
+Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects).
+
+Upon being added to an organization and projects, users assume a certain set of roles and permissions that represents their identity.
+
+
+
+## Authentication methods
+
+To interact with various resources in Infisical, users are able to utilize a number of authentication methods:
+- **Email & Password**: the most common authentication method that is used for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it.
+- **Service Tokens**: Service tokens allow users authenticate into CLI and other clients under their own identity. For the majority of use cases, it is not a recommended approach. Instead, it is often a good idea to utilize [Machine Identities](./machine-identities) with [Universal Authentication](/documentation/platform/identities/universal-auth).
+- **SSO**: Infisical natively integrates with a number of SSO identity providers like [Google](/documentation/platform/sso/google), [GitHub](/documentation/platform/sso/github), and [GitLab](/documentation/platform/sso/gitlab).
+- **SAML SSO**: It is also possible to set up SAML SSO integration with identity providers like [Okta](/documentation/platform/sso/okta), [Microsoft Entra ID](/documentation/platform/sso/azure) (formerly known as Azure AD), [JumpCloud](/documentation/platform/sso/jumpcloud), [Google](/documentation/platform/sso/google-saml), and more.
+- **LDAP**: For organizations with more advanced needs, Infisical also provides user authentication with [LDAP](/documentation/platform/ldap/overview) that includes a number of LDAP providers.
diff --git a/docs/documentation/platform/ip-allowlisting.mdx b/docs/documentation/platform/ip-allowlisting.mdx
index f0844e685..7f787fff8 100644
--- a/docs/documentation/platform/ip-allowlisting.mdx
+++ b/docs/documentation/platform/ip-allowlisting.mdx
@@ -14,7 +14,7 @@ description: "Restrict access to your secrets in Infisical using trusted IPs"
Note that IP Allowlisting is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
Projects in Infisical can be configured to restrict client access to specific IP addresses or CIDR ranges. This applies to any client using service tokens and
diff --git a/docs/documentation/platform/ldap.mdx b/docs/documentation/platform/ldap.mdx
index 01237e1c9..ba01aa743 100644
--- a/docs/documentation/platform/ldap.mdx
+++ b/docs/documentation/platform/ldap.mdx
@@ -7,7 +7,7 @@ description: "Log in to Infisical with LDAP"
LDAP is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx
index 137538cff..5e50b736b 100644
--- a/docs/documentation/platform/ldap/general.mdx
+++ b/docs/documentation/platform/ldap/general.mdx
@@ -1,12 +1,12 @@
---
title: "General LDAP"
-description: "Log in to Infisical with LDAP"
+description: "Learn how to log in to Infisical with LDAP."
---
LDAP is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx
index 5e7e42ca8..454a4d522 100644
--- a/docs/documentation/platform/ldap/jumpcloud.mdx
+++ b/docs/documentation/platform/ldap/jumpcloud.mdx
@@ -1,12 +1,12 @@
---
title: "JumpCloud LDAP"
-description: "Configure JumpCloud LDAP for Logging into Infisical"
+description: "Learn how to configure JumpCloud LDAP for authenticating into Infisical."
---
LDAP is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
diff --git a/docs/documentation/platform/ldap/overview.mdx b/docs/documentation/platform/ldap/overview.mdx
index d19095b7c..2423be8c0 100644
--- a/docs/documentation/platform/ldap/overview.mdx
+++ b/docs/documentation/platform/ldap/overview.mdx
@@ -1,6 +1,7 @@
---
title: "LDAP Overview"
-description: "Log in to Infisical with LDAP"
+sidebarTitle: "Overview"
+description: "Learn how to authenticate into Infisical with LDAP."
---
LDAP is a paid feature.
@@ -9,9 +10,9 @@ description: "Log in to Infisical with LDAP"
then you should contact sales@infisical.com to purchase an enterprise license to use it.
-You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
+You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
-To note, configuring LDAP retains the end-to-end encrypted architecture of Infisical because we decouple the authentication and decryption steps; the LDAP server cannot and will not have access to the decryption key needed to decrypt your secrets.
+To note, configuring LDAP retains the end-to-end encrypted nature of authentication in Infisical because we decouple the authentication and decryption steps; the LDAP server cannot and will not have access to the decryption key needed to decrypt your secrets.
LDAP providers:
@@ -20,4 +21,7 @@ LDAP providers:
- AWS Directory Service
- Foxpass
-Check out the general instructions for configuring LDAP [here](/documentation/platform/ldap/general).
+Read the general instructions for configuring LDAP [here](/documentation/platform/ldap/general).
+
+If the documentation for your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance.
+
diff --git a/docs/documentation/platform/mfa.mdx b/docs/documentation/platform/mfa.mdx
index 7629401eb..3ca9c5dff 100644
--- a/docs/documentation/platform/mfa.mdx
+++ b/docs/documentation/platform/mfa.mdx
@@ -1,6 +1,7 @@
---
-title: "MFA"
-description: "Secure your Infisical account with MFA"
+title: "Multi-factor Authentication"
+sidebarTitle: "MFA"
+description: "Learn how to secure your Infisical account with MFA."
---
MFA requires users to provide multiple forms of identification to access their account. Currently, this means logging in with your password and a 6-digit code sent to your email.
diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx
index 82a4058a5..d45bb6d4f 100644
--- a/docs/documentation/platform/organization.mdx
+++ b/docs/documentation/platform/organization.mdx
@@ -1,9 +1,9 @@
---
-title: "Organization"
-description: "How Infisical structures its organizations."
+title: "Organizations"
+description: "Learn more and understand the concept of Infisical organizations."
---
-An organization houses projects and members.
+An Infisical organization is a set of [projects](./project) that use the same billing. Organizations allow one or more users to control billing and project permissions for all of the projects belonging to the organization. Each project belongs to an organization.
## Projects
@@ -18,21 +18,23 @@ The **Settings** page lets you manage information about your organization includ
- Name: The name of your organization.
- Incident contacts: Emails that should be alerted if anything abnormal is detected within the organization.
-- SAML Authentication: The SAML SSO configuration of the organization (if applicable); Infisical currently
-supports Okta, Azure, and JumpCloud identity providers.

+
+
+- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations.
+

-## Members
+## Access Control
-The **Members** page is where you can manage members and their permissions within the organization.
-In the **Members** tab, you can add external members to your organization or remove them; you can also
-change their role.
+The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
+You can add or remove additional members as well as modify their permissions.
-
+
+
-In the **Roles** tab, you can manage roles for members within the organization.
+In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization.
Note that Role-Based Access Management (RBAC) is partly a paid feature.
@@ -41,13 +43,13 @@ In the **Roles** tab, you can manage roles for members within the organization.
at the organization and project level for free.
If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**.
- If you're self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it.
+ If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.

-As you can see next, Infisical supports granular permissions that you can tailor to each role. So,
-if you need certain members to only be able to access billing details, for example, then you can
+As you can see next, Infisical supports granular permissions that you can tailor to each role.
+If you need certain members to only be able to access billing details, for example, then you can
assign them that permission only.

diff --git a/docs/documentation/platform/pit-recovery.mdx b/docs/documentation/platform/pit-recovery.mdx
index 3cad5eff2..448faddbb 100644
--- a/docs/documentation/platform/pit-recovery.mdx
+++ b/docs/documentation/platform/pit-recovery.mdx
@@ -1,21 +1,21 @@
---
title: "Point-in-Time Recovery"
-description: "How to rollback secrets and configs to any commit with Infisical."
+description: "Learn how to rollback secrets and configurations to any snapshot with Infisical."
---
Point-in-Time Recovery is a paid feature.
- If you're using Infisical Cloud, then it is available under the **Team Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
-Infisical's point-in-time recovery feature allows secrets to be rolled back to any point in time for any given [folder](./folder).
-Under the hood, snapshots, capturing the state of the folder, get taken after any mutation an item within that folder.
+Infisical's point-in-time recovery functionality allows secrets to be rolled back to any point in time for any given [folder](./folder) or [environment](/documentation/platform/project#project-environments).
+Every time a secret is updated, a new snapshot is taken โ capturing the state of the folder and environment at that point of time.
## Snapshots
-Similar to Git, a commit (aka snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to
+Similar to Git, a commit (also known as snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to
an environment and [folder](./folder) within it.
To view a list of snapshots for the current folder, press the **Commits** button.
@@ -28,12 +28,14 @@ This opens up a sidebar from which you can select to view a particular snapshot:
## Rolling back
-After pressing on a snapshot from the sidebar, you can view it and even roll back the state
+After pressing on a snapshot from the sidebar, you can view it and roll back the state
of the folder to that point in time by pressing the **Rollback** button.

Rolling back secrets to a past snapshot creates a creates a snapshot at the top of the stack and updates secret versions.
-Note that rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions.
+
+Rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions.
Put differently, every [folder](./folder) possesses a distinct and separate timeline, providing granular control when managing your secrets.
+
\ No newline at end of file
diff --git a/docs/documentation/platform/pr-workflows.mdx b/docs/documentation/platform/pr-workflows.mdx
index 1c7bb9787..9df123612 100644
--- a/docs/documentation/platform/pr-workflows.mdx
+++ b/docs/documentation/platform/pr-workflows.mdx
@@ -1,6 +1,6 @@
---
-title: "PR Workflows"
-description: "Infisical PR Workflows allows you to create a set of policies to control secret operations."
+title: "Approval Workflows"
+description: "Learn how to enable a set of policies to manage changes to sensitive secrets and environments."
---
## Problem at hand
@@ -14,15 +14,15 @@ Updating secrets in high-stakes environments (e.g., production) can have a numbe
As a wide-spread software engineering practice, developers have to submit their code as a PR that needs to be approved before the code is merged into the main branch.
-In a similar way, to solve the above-mentioned issues, Infisical provides a feature called `PR Workflows` for secret management. This is a set of policies and workflows that help advance access controls, compliance procedures, and stability of a particular environment. In other words, **PR Workflows** help you secure, stabilize, and streamline the change of secrets in high-stakes environments.
+In a similar way, to solve the above-mentioned issues, Infisical provides a feature called `Approval Workflows` for secret management. This is a set of policies and workflows that help advance access controls, compliance procedures, and stability of a particular environment. In other words, **Approval Workflows** help you secure, stabilize, and streamline the change of secrets in high-stakes environments.
### Setting a policy
-First, you would need to create a set of policies for a certain environment. In the example below you can see a generic policy for a production environment. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined user (or multiple users).
+First, you would need to create a set of policies for a certain environment. In the example below, a generic policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers).

-### Example of updating secrets with PR workflows
+### Example of updating secrets with Approval workflows
When a user submits a change to an enviropnment that is under a particular policy, a corresponsing change request will go to a predefined approver (or multiple approvers).
diff --git a/docs/documentation/platform/project.mdx b/docs/documentation/platform/project.mdx
index 06ad3eaa7..bd80d8ae5 100644
--- a/docs/documentation/platform/project.mdx
+++ b/docs/documentation/platform/project.mdx
@@ -1,13 +1,21 @@
---
-title: "Project"
-description: "How Infisical organizes secrets into projects."
+title: "Projects"
+description: "Learn more and understand the concept of Infisical projects."
---
-A project houses application configuration and secrets for an application.
+A project in Infisical belongs to an [organization](./organization) and contains a number of environments, folders, and secrets.
+Only users and machine identities who belong to a project can access resources inside of it according to predefined permissions.
+
+## Project environments
+
+For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be
+customized depending on the intended use case.
+
+
## Secrets Overview
-The **Secrets Overview** page captures a birds-eye-view of secrets and folders across environments like development, staging, or production.
+The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments.
This is useful for comparing secrets, identifying if anything is missing, and making quick changes.

diff --git a/docs/documentation/platform/role-based-access-controls.mdx b/docs/documentation/platform/role-based-access-controls.mdx
deleted file mode 100644
index ba774f0d1..000000000
--- a/docs/documentation/platform/role-based-access-controls.mdx
+++ /dev/null
@@ -1,31 +0,0 @@
----
-title: "Role-based Access Controls"
-description: "Infisical's Role-based Access Controls enable creating permissions for user and machine identities to restrict access to resources and the range of actions that can be performed."
----
-
-### General access controls
-
-Access Control Policies provide a highly granular declarative way to grant or forbid access to certain resources and operations in Infisical. In general, access controls can be split up across projects and organizations.
-
-### Organization-level access controls
-
-By default, every user in a organization is either an **admin** or a **member**.
-
-Admins are able to perform every action with the organization, including adding and removing organization members, managing access controls, setting up security settings, and creating new projects. Members, on the other hand, are restricted from removing organization members, modifying billing information, updating access controls, and performing a number of other actions.
-
-Overall, organization-level access controls are significantly of administrative nature. Access to projects, secrets and other sensitive data is specified on the project level.
-
-
-
-### Project-level access controls
-
-By default, every user in a project is either a **viewer**, **developer**, or an **admin**. Each of these roles comes with a varying access to different features and resources inside projects. As such, **admins** by default have access to all environments, folders, secrets, and actions within the project. At the same time, **developers** are restricted from performing project control actions, updating PR Workflow policies, managing roles/members, and more. Lastly, **viewer** is the most limiting default role on the project level โย it forbids developers to perform any action and rather shows them in the read-only mode.
-
-### Creating custom roles
-
-By creating custom roles, you are able to adjust permissions to the needs of your organization. This can be useful for:
-- Creating superadmin roles, roles specific to SRE engineers, etc.
-- Restricting access of users to specific secrets, folders, and environments.
-- Embedding these specific roles into [PR Workflow policies](https://infisical.com/docs/documentation/platform/pr-workflows)
-
-
diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx
index 45f95e135..ff46fe4e7 100644
--- a/docs/documentation/platform/scim/azure.mdx
+++ b/docs/documentation/platform/scim/azure.mdx
@@ -1,13 +1,13 @@
---
title: "Azure SCIM"
-description: "Configure SCIM provisioning with Azure for Infisical"
+description: "Learn how to configure SCIM provisioning with Azure for Infisical."
---
Azure SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
Prerequisites:
diff --git a/docs/documentation/platform/scim/jumpcloud.mdx b/docs/documentation/platform/scim/jumpcloud.mdx
index 68bb9b66f..ce4542035 100644
--- a/docs/documentation/platform/scim/jumpcloud.mdx
+++ b/docs/documentation/platform/scim/jumpcloud.mdx
@@ -1,13 +1,13 @@
---
title: "JumpCloud SCIM"
-description: "Configure SCIM provisioning with JumpCloud for Infisical"
+description: "Learn how to configure SCIM provisioning with JumpCloud for Infisical."
---
JumpCloud SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
Prerequisites:
@@ -16,7 +16,7 @@ Prerequisites:
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
- press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users for your organization.
+ press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users and user groups for your organization.

@@ -49,7 +49,7 @@ Prerequisites:

- Now JumpCloud can provision/deprovision users to/from your organization in Infisical.
+ Now JumpCloud can provision/deprovision users and user groups to/from your organization in Infisical.
diff --git a/docs/documentation/platform/scim/okta.mdx b/docs/documentation/platform/scim/okta.mdx
index 4baa19815..6b0bf6ccf 100644
--- a/docs/documentation/platform/scim/okta.mdx
+++ b/docs/documentation/platform/scim/okta.mdx
@@ -1,13 +1,13 @@
---
title: "Okta SCIM"
-description: "Configure SCIM provisioning with Okta for Infisical"
+description: "Learn how to configure SCIM provisioning with Okta for Infisical."
---
Okta SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
Prerequisites:
@@ -16,7 +16,7 @@ Prerequisites:
In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and
- press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users for your organization.
+ press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users and user groups for your organization.

@@ -38,7 +38,7 @@ Prerequisites:
- SCIM connector base URL: Input the **SCIM URL** from Step 1.
- Unique identifier field for users: Input `email`.
- - Supported provisioning actions: Select **Push New Users** and **Push Profile Updates**.
+ - Supported provisioning actions: Select **Push New Users**, **Push Profile Updates**, and **Push Groups**.
- Authentication Mode: `HTTP Header`.

@@ -55,7 +55,7 @@ Prerequisites:

- Now Okta can provision/deprovision users to/from your organization in Infisical.
+ Now Okta can provision/deprovision users and user groups to/from your organization in Infisical.
diff --git a/docs/documentation/platform/scim/overview.mdx b/docs/documentation/platform/scim/overview.mdx
index deec8b630..232df95a1 100644
--- a/docs/documentation/platform/scim/overview.mdx
+++ b/docs/documentation/platform/scim/overview.mdx
@@ -1,16 +1,16 @@
---
title: "SCIM Overview"
-description: "Provision users for Infisical via SCIM"
+description: "Learn how to provision users for Infisical via SCIM."
---
SCIM provisioning is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
-You can configure your organization in Infisical to have members be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
+You can configure your organization in Infisical to have users and user groups be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc.
- Provisioning: The SCIM provider pushes user information to Infisical. If the user exists in Infisical, Infisical sends an email invitation to add them to the relevant organization in Infisical; if not, Infisical initializes a new user and sends them an email invitation to finish setting up their account in the organization.
- Deprovisioning: The SCIM provider instructs Infisical to remove user(s) from an organization in Infisical.
diff --git a/docs/documentation/platform/secret-reference.mdx b/docs/documentation/platform/secret-reference.mdx
index 66961db99..042f7525b 100644
--- a/docs/documentation/platform/secret-reference.mdx
+++ b/docs/documentation/platform/secret-reference.mdx
@@ -1,11 +1,12 @@
---
-title: "Secret Referencing / Importing"
-description: "How to use reference secrets in Infisical"
+title: "Secret Referencing and Importing"
+sidebarTitle: "Referencing and Importing"
+description: "Learn the fundamentals of secret referencing and importing in Infisical."
---
## Secret Referencing
-Infisical's secret referencing feature lets you reference the value of a "base" secret when defining the value of another secret.
+Infisical's secret referencing functionality makes it possible to reference the value of a "base" secret when defining the value of another secret.
This means that updating the value of a base secret propagates directly to other secrets whose values depend on the base secret.
@@ -43,7 +44,7 @@ Here are a few more helpful examples for how to reference secrets in different c
## Secret Imports
-Infisical's secret imports feature lets you import the items of another environment or folder into the current folder context.
+Infisical's Secret Imports functionality makes it possible to import the secrets from another environment or folder into the current folder context.
This can be useful if you have common secrets that need to be available across multiple environments/folders.
To add a secret import, press the downward chevron to the right of the **Add Secret** button; then press on the **Add Import** button.
diff --git a/docs/documentation/platform/secret-rotation/aws-iam.mdx b/docs/documentation/platform/secret-rotation/aws-iam.mdx
index b4247af80..c524abfbc 100644
--- a/docs/documentation/platform/secret-rotation/aws-iam.mdx
+++ b/docs/documentation/platform/secret-rotation/aws-iam.mdx
@@ -1,6 +1,6 @@
---
title: "AWS IAM User"
-description: "Rotated access key id and secret key of AWS IAM Users"
+description: "Learn how to automatically rotate Access Key Id and Secret Key of AWS IAM Users."
---
Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical
diff --git a/docs/documentation/platform/secret-rotation/mysql.mdx b/docs/documentation/platform/secret-rotation/mysql.mdx
index b630e349a..02356de48 100644
--- a/docs/documentation/platform/secret-rotation/mysql.mdx
+++ b/docs/documentation/platform/secret-rotation/mysql.mdx
@@ -1,37 +1,102 @@
---
title: "MySQL/MariaDB"
-description: "Rotated database user password of a MySQL or MariaDB"
+description: "Learn how to automatically rotate MySQL/MariaDB user passwords."
---
-Infisical will update periodically the provided database user's password.
+The Infisical MySQL secret rotation allows you to automatically rotate your MySQL database user's password at a predefined interval.
-
- At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud.
-
-## Working
+## Prerequisite
-1. User's has to create the two user's for Infisical to rotate and provide them required database access
-2. Infisical will connect with your database with admin access
-3. If last rotated one was username1, then username2 is chosen to be rotated
-5. Update it's password with random value
-6. After testing it gets saved to the provided secret mapping
+1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`.
+2. Create another MySQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
+
+To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html).
+
+## How it works
+
+1. Infisical connects to your database using the provided `admin` user account.
+2. A random value is generated and the password for `user-a` is updated with the new value.
+3. The new password is then tested by logging into the database
+4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
+5. The process is then repeated for `user-b` on the next rotation.
+6. The cycle repeats until secret rotation is deleted/stopped.
## Rotation Configuration
-1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
-2. Click on `MySQL`
-3. Provide the inputs
- - Admin Username: DB admin username
- - Admin Password: DB admin password
- - Host: DB host
- - Port: DB port(number)
- - Username1: The first username in two to rotate
- - Username2: The second username in two to rotate
- - CA: Certificate to connect with database(string)
-4. Final step
- - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- - Finally select the secrets in your provided board to replace with new secret after each rotation
- - Your done and good to go.
+
+
+ Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
+
+
+
+
+ Rotator admin username
+
-Congrats. You have 10x your MySQL/MariaDB access security.
+
+ Rotator admin password
+
+
+
+ Database host url
+
+
+
+ Database port number
+
+
+
+ The first username of two to rotate - `user-a`
+
+
+
+ The second username of two to rotate - `user-b`
+
+
+
+ Optional database certificate to connect with database
+
+
+
+
+ When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
+
+
+ The environment where the rotated credentials should be mapped to.
+
+
+
+ The secret path where the rotated credentials should be mapped to.
+
+
+
+ What interval should the credentials be rotated in days.
+
+
+
+ Select an existing secret key where the rotated database username value should be saved to.
+
+
+
+ Select an existing select key where the rotated database password value should be saved to.
+
+
+
+
+## FAQ
+
+
+
+ When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
+
+ This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
+
+ To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
+
+
+ The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
+
+ You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
+
+
diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx
index 284375401..57ad17e09 100644
--- a/docs/documentation/platform/secret-rotation/overview.mdx
+++ b/docs/documentation/platform/secret-rotation/overview.mdx
@@ -1,4 +1,8 @@
-# Secret Rotation Overview
+---
+title: "Secret Rotation"
+sidebarTitle: "Overview"
+description: "Learn how to set up automated secret rotation in Infisical."
+---
## Introduction
@@ -7,8 +11,8 @@ Rotating secrets helps prevent unauthorized access to systems and sensitive data
Rotated secrets may include, but are not limited to:
-1. API keys for external services
-2. Database credentials for various platforms
+1. API keys for external services;
+2. Database credentials for various platforms.
## Rotation Process
@@ -42,3 +46,4 @@ Finally, the system promotes the future active (pending) secret to be the new cu
1. [SendGrid Integration](./sendgrid)
2. [PostgreSQL/CockroachDB Implementation](./postgres)
3. [MySQL/MariaDB Configuration](./mysql)
+4. [AWS IAM User](./aws-iam)
diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx
index b11ae1d76..0a6339e4e 100644
--- a/docs/documentation/platform/secret-rotation/postgres.mdx
+++ b/docs/documentation/platform/secret-rotation/postgres.mdx
@@ -1,33 +1,104 @@
---
title: "PostgreSQL/CockroachDB"
-description: "Rotated database user password of a PostgreSQL or Cockroach DB"
+description: "Learn how to automatically rotate PostgreSQL/CockroachDB user passwords."
---
-Infisical will update periodically the provided database user's password.
+The Infisical Postgres secret rotation allows you to automatically rotate your Postgres database user's password at a predefined interval.
-## Working
-1. User's has to create the two user's for Infisical to rotate and provide them required database access.
-2. Infisical will connect with your database with admin access.
-3. If last rotated one was username1, then username2 is chosen to be rotated.
-5. Update it's password with random value.
-6. After testing it gets saved to the provided secret mapping.
+## Prerequisite
+
+1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`.
+2. Create another PostgreSQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
+
+To learn more about Postgres permission system, please visit this [documentation](https://www.postgresql.org/docs/9.1/sql-grant.html).
+
+
+## How it works
+
+1. Infisical connects to your database using the provided `admin` user account.
+2. A random value is generated and the password for `user-a` is updated with the new value.
+3. The new password is then tested by logging into the database
+4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
+5. The process is then repeated for `user-b` on the next rotation.
+6. The cycle repeats until secret rotation is deleted/stopped.
## Rotation Configuration
-1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
-2. Click on `PostgreSQL`
-3. Provide the inputs
- - Admin Username: DB admin username
- - Admin Password: DB admin password
- - Host: DB host
- - Port: DB port(number)
- - Username1: The first username in two to rotate
- - Username2: The second username in two to rotate
- - CA: Certificate to connect with database(string)
-4. Final step
- - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- - Finally select the secrets in your provided board to replace with new secret after each rotation
- - Your done and good to go.
+
+
+ Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
+
+
-Congratulations. You have improved your PostgreSQL/CockroachDB access security.
+
+
+ Rotator admin username
+
+
+
+ Rotator admin password
+
+
+
+ Database host url
+
+
+
+ Database port number
+
+
+
+ The first username of two to rotate - `user-a`
+
+
+
+ The second username of two to rotate - `user-b`
+
+
+
+ Optional database certificate to connect with database
+
+
+
+
+ When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
+
+
+ The environment where the rotated credentials should be mapped to.
+
+
+
+ The secret path where the rotated credentials should be mapped to.
+
+
+
+ What interval should the credentials be rotated in days.
+
+
+
+ Select an existing secret key where the rotated database username value should be saved to.
+
+
+
+ Select an existing select key where the rotated database password value should be saved to.
+
+
+
+
+## FAQ
+
+
+
+ When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
+
+ This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
+
+ To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
+
+
+ The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
+
+ You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
+
+
diff --git a/docs/documentation/platform/secret-rotation/sendgrid.mdx b/docs/documentation/platform/secret-rotation/sendgrid.mdx
index c4dd2797f..4f27057ad 100644
--- a/docs/documentation/platform/secret-rotation/sendgrid.mdx
+++ b/docs/documentation/platform/secret-rotation/sendgrid.mdx
@@ -1,31 +1,58 @@
---
title: "Twilio SendGrid"
-description: "Rotate Twilio SendGrid API keys"
+description: "Find out how to rotate Twilio SendGrid API keys."
---
-Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails.
-It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys.
+Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical.
-## Working
+## Prerequisite
-1. Infisical will need an admin token of SendGrid to create API keys dynamically.
-2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically
-3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys)
+You will need a valid SendGrid admin key with the necessary scope to create additional API keys.
+
+Follow the [SendGrid Docs to create an admin api key](https://docs.sendgrid.com/ui/account-and-settings/api-keys).
+
+## How it works
+
+Using the provided admin API key, Infisical will attempt to create child API keys with the specified permissions.
+New keys will ge generated every time a rotation occurs. Behind the scenes, Infisical uses the [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) to generate new API keys.
## Rotation Configuration
-1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
-2. Click on `Twilio SendGrid Card`
-3. Provide the inputs
- - Admin API Key:
- SendGrid admin key to create lower scoped API keys.
- - API Key Scopes
- SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions)
+
+
+ Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
+
+
+
+
+ SendGrid admin API key with permission to create child scoped API keys.
+
-4. Final step
- - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- - Finally select the secrets in your provided board to replace with new secret after each rotation
- - Your done and good to go.
-
-Now your output mapped secret value will be replaced periodically by SendGrid.
+
+ The permissions that the newly generated API keys will have. To view possible permissions, visit [this documentation](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions).
+ Permissions must be entered as a list of strings.
+ Example: `["user.profile.read", "user.profile.update"]`
+
+
+
+ When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
+
+ The environment where the rotated credentials should be mapped to.
+
+
+
+ The secret path where the rotated credentials should be mapped to.
+
+
+
+ What interval should the credentials be rotated in days.
+
+
+
+ Select an existing select key where the newly rotated API key will get saved to.
+
+
+
+
+Now your output mapped secret value will be replaced periodically by SendGrid.
diff --git a/docs/documentation/platform/secret-versioning.mdx b/docs/documentation/platform/secret-versioning.mdx
index 04c3086cd..741bbc308 100644
--- a/docs/documentation/platform/secret-versioning.mdx
+++ b/docs/documentation/platform/secret-versioning.mdx
@@ -1,15 +1,20 @@
---
title: "Secret Versioning"
-description: "Version secrets and configurations with Infisical"
+description: "Learn how secret versioning works in Infisical."
---
-Secret versioning records changes made to every secret.
+Every time a secret change is persformed, a new version of the same secret is created.
-
+Such versions can be accessed visually by opening up the [secret sidebar](/documentation/platform/project#drawer) (as seen below) or [retrived via API](/api-reference/endpoints/secrets/read)
+by specifying the `version` query parameter.
+
+
+
+The secret versioning functionality is heavily connected to [Point-in-time Recovery](/documentation/platform/pit-recovery) of secrets in Infisical.
You can copy and paste a secret version value to the "Value" input field "roll
back" to that secret version. This creates a new secret version at the top of
- the stack. We're releasing the ability to press and automatically roll back to
+ the stack. We're releasing the ability to automatically roll back to
a secret version soon.
diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx
index 30622e6e8..cbd5a7d0e 100644
--- a/docs/documentation/platform/sso/azure.mdx
+++ b/docs/documentation/platform/sso/azure.mdx
@@ -1,13 +1,13 @@
---
-title: "Azure SAML"
-description: "Configure Azure SAML for Infisical SSO"
+title: "Entra ID / Azure AD SAML"
+description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
---
Azure SAML SSO is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
diff --git a/docs/documentation/platform/sso/github.mdx b/docs/documentation/platform/sso/github.mdx
index 87d1b3cf7..53a9b9156 100644
--- a/docs/documentation/platform/sso/github.mdx
+++ b/docs/documentation/platform/sso/github.mdx
@@ -1,6 +1,6 @@
---
title: "GitHub SSO"
-description: "Configure GitHub SSO for Infisical"
+description: "Learn how to configure GitHub SSO for Infisical."
---
Using GitHub SSO on a self-hosted instance of Infisical requires configuring an OAuth2 application in GitHub and registering your instance with it.
diff --git a/docs/documentation/platform/sso/gitlab.mdx b/docs/documentation/platform/sso/gitlab.mdx
index 446758ae0..d2a537bfa 100644
--- a/docs/documentation/platform/sso/gitlab.mdx
+++ b/docs/documentation/platform/sso/gitlab.mdx
@@ -1,6 +1,6 @@
---
title: "GitLab SSO"
-description: "Configure GitLab SSO for Infisical"
+description: "Learn how to configure GitLab SSO for Infisical."
---
Using GitLab SSO on a self-hosted instance of Infisical requires configuring an OAuth application in GitLab and registering your instance with it.
diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx
index 743c4e3ff..1897a651a 100644
--- a/docs/documentation/platform/sso/google-saml.mdx
+++ b/docs/documentation/platform/sso/google-saml.mdx
@@ -1,13 +1,13 @@
---
title: "Google SAML"
-description: "Configure Google SAML for Infisical SSO"
+description: "Learn how to configure Google SAML for Infisical SSO."
---
Google SAML SSO feature is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
diff --git a/docs/documentation/platform/sso/google.mdx b/docs/documentation/platform/sso/google.mdx
index cf35dcb68..36ee511d1 100644
--- a/docs/documentation/platform/sso/google.mdx
+++ b/docs/documentation/platform/sso/google.mdx
@@ -1,6 +1,6 @@
---
title: "Google SSO"
-description: "Configure Google SSO for Infisical"
+description: "Learn how to configure Google SSO for Infisical."
---
Using Google SSO on a self-hosted instance of Infisical requires configuring an OAuth2 application in GCP and registering your instance with it.
diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx
index 8b64c8643..781f5224a 100644
--- a/docs/documentation/platform/sso/jumpcloud.mdx
+++ b/docs/documentation/platform/sso/jumpcloud.mdx
@@ -1,13 +1,13 @@
---
title: "JumpCloud SAML"
-description: "Configure JumpCloud SAML for Infisical SSO"
+description: "Learn how to configure JumpCloud SAML for Infisical SSO."
---
JumpCloud SAML SSO is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx
new file mode 100644
index 000000000..981739711
--- /dev/null
+++ b/docs/documentation/platform/sso/keycloak-saml.mdx
@@ -0,0 +1,139 @@
+---
+title: "Keycloak SAML"
+description: "Learn how to configure Keycloak SAML for Infisical SSO."
+---
+
+
+ Keycloak SAML SSO is a paid feature.
+
+ If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
+
+
+
+
+ In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Manage**.
+
+ 
+
+ Next, copy the **Valid redirect URI** and **SP Entity ID** to use when configuring the Keycloak SAML application.
+
+ 
+
+
+ 2.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application.
+
+ 
+
+
+ You donโt typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm.
+
+
+ In the General Settings step, set **Client type** to **SAML**, the **Client ID** field to `https://app.infisical.com`, and the **Name** field to a friendly name like **Infisical**.
+
+ 
+
+
+ If youโre self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain.
+
+
+ Next, in the Login Settings step, set both the **Home URL** field and **Valid redirect URIs** field to the **Valid redirect URI** from step 1 and press **Save**.
+
+ 
+
+ 2.2. Once you've created the client, under its **Settings** tab, make sure to set the following values:
+
+ - Under **SAML Capabilities**:
+ - Name ID format: email (or username).
+ - Force name ID format: On.
+ - Force POST binding: On.
+ - Include AuthnStatement: On.
+ - Under **Signature and Encryption**:
+ - Sign documents: On.
+ - Sign assertions: On.
+ - Signature algorithm: RSA_SHA256.
+
+ 
+
+ 
+
+ 2.3. Next, navigate to the **Client scopes** tab select the client's dedicated scope.
+
+ 
+
+ Next click **Add predefined mapper**.
+
+ 
+
+ Select the **X500 email**, **X500 givenName**, and **X500 surname** attributes and click **Add**.
+
+ 
+
+ Now click on the **X500 email** mapper and set the **SAML Attribute Name** field to **email**.
+
+ 
+
+ Repeat the same for **X500 givenName** and **X500 surname** mappers, setting the **SAML Attribute Name** field to **firstName** and **lastName** respectively.
+
+ Next, back in the client scope's **Mappers**, click **Add mapper** and select **by configuration**.
+
+ 
+
+ Select **User Property**.
+
+ 
+
+ Set the the **Name** field to **Username**, the **Property** field to **username**, and the **SAML Attribtue Name** to **username**.
+
+ 
+
+ Repeat the same for the `id` attribute, setting the **Name** field to **ID**, the **Property** field to **id**, and the **SAML Attribute Name** to **id**.
+
+ 
+
+ Once you've completed the above steps, the list of mappers should look like this:
+
+ 
+
+
+ Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > SAML 2.0 Identity Provider Metadata and copy the IDP URL. This should appear in various places and take the form: `https://keycloak-mysite.com/realms/myrealm/protocol/saml`.
+
+ 
+
+ Also, in the **Keys** tab, locate the RS256 key and copy the certificate to use when finishing configuring Keycloak SAML in Infisical.
+
+ 
+
+
+ Back in Infisical, set **IDP URL** and **Certificate** to the items from step 3. Also, set the **Client ID** to the `https://app.infisical.com`.
+
+ Once you've done that, press **Update** to complete the required configuration.
+
+ 
+
+
+ Enabling SAML SSO allows members in your organization to log into Infisical via Keycloak.
+
+ 
+
+
+ Enforcing SAML SSO ensures that members in your organization can only access Infisical
+ by logging into the organization via Keycloak.
+
+ To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Keycloak user with Infisical;
+ Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO.
+
+
+ We recommend ensuring that your account is provisioned the application in Keycloak
+ prior to enforcing SAML SSO to prevent any unintended issues.
+
+
+
+
+
+ If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
+ set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work:
+
+ - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`.
+ - `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
+
\ No newline at end of file
diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx
index f18203963..c81141c92 100644
--- a/docs/documentation/platform/sso/okta.mdx
+++ b/docs/documentation/platform/sso/okta.mdx
@@ -1,13 +1,13 @@
---
title: "Okta SAML"
-description: "Configure Okta SAML 2.0 for Infisical SSO"
+description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
---
Okta SAML SSO is a paid feature.
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
- then you should contact team@infisical.com to purchase an enterprise license to use it.
+ then you should contact sales@infisical.com to purchase an enterprise license to use it.
diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx
index e1fd25957..6064f26e8 100644
--- a/docs/documentation/platform/sso/overview.mdx
+++ b/docs/documentation/platform/sso/overview.mdx
@@ -1,6 +1,7 @@
---
title: "SSO Overview"
-description: "Log in to Infisical via SSO protocols"
+sidebarTitle: "Overview"
+description: "Learn how to log in to Infisical via SSO protocols."
---
@@ -13,8 +14,12 @@ description: "Log in to Infisical via SSO protocols"
You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
-To note, configuring SSO retains the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps. In all login with SSO implementations,
-your IdP cannot and will not have access to the decryption key needed to decrypt your secrets.
+To note, Infisical's SSO implementation decouples the **authentication** and **decryption** steps โย which implies that no
+Identity Provider can have access to the decryption key needed to decrypt your secrets (this also implies that Infisical requires entering the user's Master Password on top of authenticating with SSO).
+
+## Identity providers
+
+Infisical supports these and many other identity providers:
- [Google SSO](/documentation/platform/sso/google)
- [GitHub SSO](/documentation/platform/sso/github)
@@ -22,4 +27,7 @@ your IdP cannot and will not have access to the decryption key needed to decrypt
- [Okta SAML](/documentation/platform/sso/okta)
- [Azure SAML](/documentation/platform/sso/azure)
- [JumpCloud SAML](/documentation/platform/sso/jumpcloud)
+- [Keycloak SAML](/documentation/platform/sso/keycloak-saml)
- [Google SAML](/documentation/platform/sso/google-saml)
+
+If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance.
diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx
index 9e304de3d..78445f4f1 100644
--- a/docs/documentation/platform/token.mdx
+++ b/docs/documentation/platform/token.mdx
@@ -1,6 +1,6 @@
---
-title: "Service token"
-description: "Infisical service tokens allows you to programmatically interact with Infisical"
+title: "Service Token"
+description: "Infisical service tokens allow users to programmatically interact with Infisical."
---
Service tokens are authentication credentials that services can use to access designated endpoints in the Infisical API to manage project resources like secrets.
@@ -43,6 +43,10 @@ Also, note that Infisical supports [glob patterns](https://www.malikbrowne.com/b
In the above screenshot, you can see that we are creating a token token with `read` access to all subfolders at any depth
of the `/common` path within the development environment of the project; the token expires in 6 months and can be used from any IP address.
+
+For a deeper understanding of service tokens, it is recommended to read [this guide](https://infisical.com/docs/internals/service-tokens).
+
+
**FAQ**
diff --git a/docs/documentation/platform/webhooks.mdx b/docs/documentation/platform/webhooks.mdx
index e0de7be05..22277dd8c 100644
--- a/docs/documentation/platform/webhooks.mdx
+++ b/docs/documentation/platform/webhooks.mdx
@@ -1,6 +1,6 @@
---
title: "Webhooks"
-description: "How Infisical webhooks works?"
+description: "Learn the fundamentals of Infisical webhooks."
---
Webhooks can be used to trigger changes to your integrations when secrets are modified, providing smooth integration with other third-party applications.
diff --git a/docs/images/agent/infisical-agent-diagram.png b/docs/images/agent/infisical-agent-diagram.png
index 27356ba11..5eab132f7 100644
Binary files a/docs/images/agent/infisical-agent-diagram.png and b/docs/images/agent/infisical-agent-diagram.png differ
diff --git a/docs/images/auth-methods/access-personal-settings.png b/docs/images/auth-methods/access-personal-settings.png
new file mode 100644
index 000000000..a5e1989c1
Binary files /dev/null and b/docs/images/auth-methods/access-personal-settings.png differ
diff --git a/docs/images/guides/microsoft-power-apps/custom-connector.png b/docs/images/guides/microsoft-power-apps/custom-connector.png
new file mode 100644
index 000000000..e74fe61f1
Binary files /dev/null and b/docs/images/guides/microsoft-power-apps/custom-connector.png differ
diff --git a/docs/images/guides/microsoft-power-apps/function-app.png b/docs/images/guides/microsoft-power-apps/function-app.png
new file mode 100644
index 000000000..9b92cdc97
Binary files /dev/null and b/docs/images/guides/microsoft-power-apps/function-app.png differ
diff --git a/docs/images/integrations/aws/integrations-amplify-app-id.png b/docs/images/integrations/aws/integrations-amplify-app-id.png
new file mode 100644
index 000000000..a89fa3eaf
Binary files /dev/null and b/docs/images/integrations/aws/integrations-amplify-app-id.png differ
diff --git a/docs/images/integrations/aws/integrations-amplify-env-console.png b/docs/images/integrations/aws/integrations-amplify-env-console.png
new file mode 100644
index 000000000..10791acf7
Binary files /dev/null and b/docs/images/integrations/aws/integrations-amplify-env-console.png differ
diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png b/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png
index 4dcaa04dd..cc17097e1 100644
Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png differ
diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png
index 703fb6101..21f2213ef 100644
Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png differ
diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png
new file mode 100644
index 000000000..f8492cdfa
Binary files /dev/null and b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png differ
diff --git a/docs/images/integrations/github/integrations-github-scope-env.png b/docs/images/integrations/github/integrations-github-scope-env.png
new file mode 100644
index 000000000..e38874bd3
Binary files /dev/null and b/docs/images/integrations/github/integrations-github-scope-env.png differ
diff --git a/docs/images/integrations/github/integrations-github-scope-org.png b/docs/images/integrations/github/integrations-github-scope-org.png
new file mode 100644
index 000000000..d5ef76a2b
Binary files /dev/null and b/docs/images/integrations/github/integrations-github-scope-org.png differ
diff --git a/docs/images/integrations/github/integrations-github-scope-repo.png b/docs/images/integrations/github/integrations-github-scope-repo.png
new file mode 100644
index 000000000..353527c78
Binary files /dev/null and b/docs/images/integrations/github/integrations-github-scope-repo.png differ
diff --git a/docs/images/integrations/github/integrations-github.png b/docs/images/integrations/github/integrations-github.png
index dccc42c0d..38550b466 100644
Binary files a/docs/images/integrations/github/integrations-github.png and b/docs/images/integrations/github/integrations-github.png differ
diff --git a/docs/images/organization-members.png b/docs/images/organization-members.png
deleted file mode 100644
index 70190df0c..000000000
Binary files a/docs/images/organization-members.png and /dev/null differ
diff --git a/docs/images/platform/access-controls/access-request-policies.png b/docs/images/platform/access-controls/access-request-policies.png
new file mode 100644
index 000000000..d7ea4829c
Binary files /dev/null and b/docs/images/platform/access-controls/access-request-policies.png differ
diff --git a/docs/images/platform/access-controls/access-requests-completed.png b/docs/images/platform/access-controls/access-requests-completed.png
new file mode 100644
index 000000000..a2a167f78
Binary files /dev/null and b/docs/images/platform/access-controls/access-requests-completed.png differ
diff --git a/docs/images/platform/access-controls/access-requests-pending.png b/docs/images/platform/access-controls/access-requests-pending.png
new file mode 100644
index 000000000..d75f669e2
Binary files /dev/null and b/docs/images/platform/access-controls/access-requests-pending.png differ
diff --git a/docs/images/platform/access-controls/add-additional-privileges.png b/docs/images/platform/access-controls/add-additional-privileges.png
new file mode 100644
index 000000000..28848075a
Binary files /dev/null and b/docs/images/platform/access-controls/add-additional-privileges.png differ
diff --git a/docs/images/platform/access-controls/additional-privileges.png b/docs/images/platform/access-controls/additional-privileges.png
new file mode 100644
index 000000000..4561021ba
Binary files /dev/null and b/docs/images/platform/access-controls/additional-privileges.png differ
diff --git a/docs/images/platform/access-controls/configure-temporary-access.png b/docs/images/platform/access-controls/configure-temporary-access.png
new file mode 100644
index 000000000..0c16bbc35
Binary files /dev/null and b/docs/images/platform/access-controls/configure-temporary-access.png differ
diff --git a/docs/images/platform/access-controls/confirm-additional-privileges.png b/docs/images/platform/access-controls/confirm-additional-privileges.png
new file mode 100644
index 000000000..b6fdbf518
Binary files /dev/null and b/docs/images/platform/access-controls/confirm-additional-privileges.png differ
diff --git a/docs/images/platform/access-controls/create-access-request-policy.png b/docs/images/platform/access-controls/create-access-request-policy.png
new file mode 100644
index 000000000..6593fd733
Binary files /dev/null and b/docs/images/platform/access-controls/create-access-request-policy.png differ
diff --git a/docs/images/platform/access-controls/edit-role.png b/docs/images/platform/access-controls/edit-role.png
new file mode 100644
index 000000000..598f585b8
Binary files /dev/null and b/docs/images/platform/access-controls/edit-role.png differ
diff --git a/docs/images/platform/access-controls/rbac.png b/docs/images/platform/access-controls/rbac.png
new file mode 100644
index 000000000..22380c805
Binary files /dev/null and b/docs/images/platform/access-controls/rbac.png differ
diff --git a/docs/images/platform/access-controls/request-access.png b/docs/images/platform/access-controls/request-access.png
new file mode 100644
index 000000000..63c76dbd5
Binary files /dev/null and b/docs/images/platform/access-controls/request-access.png differ
diff --git a/docs/images/platform/access-controls/review-access-request.png b/docs/images/platform/access-controls/review-access-request.png
new file mode 100644
index 000000000..8376f9691
Binary files /dev/null and b/docs/images/platform/access-controls/review-access-request.png differ
diff --git a/docs/images/platform/access-controls/temporary-access.png b/docs/images/platform/access-controls/temporary-access.png
new file mode 100644
index 000000000..24be8a584
Binary files /dev/null and b/docs/images/platform/access-controls/temporary-access.png differ
diff --git a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png
new file mode 100644
index 000000000..8d0fd3ecc
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-generate.png b/docs/images/platform/dynamic-secrets/dynamic-secret-generate.png
new file mode 100644
index 000000000..4a816614a
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-generate.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png
new file mode 100644
index 000000000..e6da94dcd
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png
new file mode 100644
index 000000000..e97554415
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-mysql.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-mysql.png
new file mode 100644
index 000000000..de4911d0d
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-mysql.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-oracle.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-oracle.png
new file mode 100644
index 000000000..053873a9c
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-oracle.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal.png
new file mode 100644
index 000000000..5f487dd7f
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal.png
new file mode 100644
index 000000000..3cd6c06f6
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal.png differ
diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret.png b/docs/images/platform/dynamic-secrets/dynamic-secret.png
new file mode 100644
index 000000000..e1ec71fcd
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret.png differ
diff --git a/docs/images/platform/dynamic-secrets/lease-data.png b/docs/images/platform/dynamic-secrets/lease-data.png
new file mode 100644
index 000000000..aecd8c11d
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/lease-data.png differ
diff --git a/docs/images/platform/dynamic-secrets/lease-values.png b/docs/images/platform/dynamic-secrets/lease-values.png
new file mode 100644
index 000000000..d552845f8
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/lease-values.png differ
diff --git a/docs/images/platform/dynamic-secrets/modify-sql-statement-mysql.png b/docs/images/platform/dynamic-secrets/modify-sql-statement-mysql.png
new file mode 100644
index 000000000..8ad9fc0e3
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/modify-sql-statement-mysql.png differ
diff --git a/docs/images/platform/dynamic-secrets/modify-sql-statement-oracle.png b/docs/images/platform/dynamic-secrets/modify-sql-statement-oracle.png
new file mode 100644
index 000000000..0874aa23d
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/modify-sql-statement-oracle.png differ
diff --git a/docs/images/platform/dynamic-secrets/modify-sql-statements.png b/docs/images/platform/dynamic-secrets/modify-sql-statements.png
new file mode 100644
index 000000000..d0f3b09da
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/modify-sql-statements.png differ
diff --git a/docs/images/platform/dynamic-secrets/provision-lease.png b/docs/images/platform/dynamic-secrets/provision-lease.png
new file mode 100644
index 000000000..f144a5ae2
Binary files /dev/null and b/docs/images/platform/dynamic-secrets/provision-lease.png differ
diff --git a/docs/images/platform/groups/groups-org-create.png b/docs/images/platform/groups/groups-org-create.png
new file mode 100644
index 000000000..a8a1e677c
Binary files /dev/null and b/docs/images/platform/groups/groups-org-create.png differ
diff --git a/docs/images/platform/groups/groups-org-users-assign.png b/docs/images/platform/groups/groups-org-users-assign.png
new file mode 100644
index 000000000..b5f629c2e
Binary files /dev/null and b/docs/images/platform/groups/groups-org-users-assign.png differ
diff --git a/docs/images/platform/groups/groups-org-users.png b/docs/images/platform/groups/groups-org-users.png
new file mode 100644
index 000000000..383425e77
Binary files /dev/null and b/docs/images/platform/groups/groups-org-users.png differ
diff --git a/docs/images/platform/groups/groups-org.png b/docs/images/platform/groups/groups-org.png
new file mode 100644
index 000000000..13b2edc44
Binary files /dev/null and b/docs/images/platform/groups/groups-org.png differ
diff --git a/docs/images/platform/groups/groups-project-create.png b/docs/images/platform/groups/groups-project-create.png
new file mode 100644
index 000000000..9232aa042
Binary files /dev/null and b/docs/images/platform/groups/groups-project-create.png differ
diff --git a/docs/images/platform/groups/groups-project.png b/docs/images/platform/groups/groups-project.png
new file mode 100644
index 000000000..83e384861
Binary files /dev/null and b/docs/images/platform/groups/groups-project.png differ
diff --git a/docs/images/platform/organization/organization-machine-identities.png b/docs/images/platform/organization/organization-machine-identities.png
new file mode 100644
index 000000000..17bea6e9b
Binary files /dev/null and b/docs/images/platform/organization/organization-machine-identities.png differ
diff --git a/docs/images/platform/organization/organization-members-roles.png b/docs/images/platform/organization/organization-members-roles.png
index 454af0809..08c2d1e90 100644
Binary files a/docs/images/platform/organization/organization-members-roles.png and b/docs/images/platform/organization/organization-members-roles.png differ
diff --git a/docs/images/platform/organization/organization-members.png b/docs/images/platform/organization/organization-members.png
new file mode 100644
index 000000000..a79d3bbe0
Binary files /dev/null and b/docs/images/platform/organization/organization-members.png differ
diff --git a/docs/images/platform/organization/organization-settings-auth.png b/docs/images/platform/organization/organization-settings-auth.png
index 8643c44da..ca2340e9f 100644
Binary files a/docs/images/platform/organization/organization-settings-auth.png and b/docs/images/platform/organization/organization-settings-auth.png differ
diff --git a/docs/images/platform/project/project-environments.png b/docs/images/platform/project/project-environments.png
new file mode 100644
index 000000000..e468f2b82
Binary files /dev/null and b/docs/images/platform/project/project-environments.png differ
diff --git a/docs/images/platform/scim/okta/scim-okta-config.png b/docs/images/platform/scim/okta/scim-okta-config.png
index b20ceddca..bca1a25eb 100644
Binary files a/docs/images/platform/scim/okta/scim-okta-config.png and b/docs/images/platform/scim/okta/scim-okta-config.png differ
diff --git a/docs/images/platform/secret-versioning.png b/docs/images/platform/secret-versioning.png
new file mode 100644
index 000000000..593e8c96f
Binary files /dev/null and b/docs/images/platform/secret-versioning.png differ
diff --git a/docs/images/secret-rotation/mysql-step1.png b/docs/images/secret-rotation/mysql-step1.png
new file mode 100644
index 000000000..316dd3adf
Binary files /dev/null and b/docs/images/secret-rotation/mysql-step1.png differ
diff --git a/docs/images/secret-rotation/postgres-step1.png b/docs/images/secret-rotation/postgres-step1.png
new file mode 100644
index 000000000..8b64932ea
Binary files /dev/null and b/docs/images/secret-rotation/postgres-step1.png differ
diff --git a/docs/images/secret-rotation/postgres-step2.png b/docs/images/secret-rotation/postgres-step2.png
new file mode 100644
index 000000000..b261e7464
Binary files /dev/null and b/docs/images/secret-rotation/postgres-step2.png differ
diff --git a/docs/images/secret-rotation/sendgrid-step1.png b/docs/images/secret-rotation/sendgrid-step1.png
new file mode 100644
index 000000000..cb919e34f
Binary files /dev/null and b/docs/images/secret-rotation/sendgrid-step1.png differ
diff --git a/docs/images/secret-rotation/sendgrid-step2.png b/docs/images/secret-rotation/sendgrid-step2.png
new file mode 100644
index 000000000..62c1f29ff
Binary files /dev/null and b/docs/images/secret-rotation/sendgrid-step2.png differ
diff --git a/docs/images/self-hosting/configuration/email/ses-create-identity.png b/docs/images/self-hosting/configuration/email/ses-create-identity.png
new file mode 100644
index 000000000..58b2b2e24
Binary files /dev/null and b/docs/images/self-hosting/configuration/email/ses-create-identity.png differ
diff --git a/docs/images/self-hosting/reference-architectures/Infisical-AWS-ECS-architecture.jpeg b/docs/images/self-hosting/reference-architectures/Infisical-AWS-ECS-architecture.jpeg
new file mode 100644
index 000000000..2c63045ec
Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/Infisical-AWS-ECS-architecture.jpeg differ
diff --git a/docs/images/self-hosting/reference-architectures/on-premise-architecture.png b/docs/images/self-hosting/reference-architectures/on-premise-architecture.png
new file mode 100644
index 000000000..a4d04f98d
Binary files /dev/null and b/docs/images/self-hosting/reference-architectures/on-premise-architecture.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-by-configuration.png b/docs/images/sso/keycloak/client-mappers-by-configuration.png
new file mode 100644
index 000000000..9bebb422e
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-by-configuration.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-completed.png b/docs/images/sso/keycloak/client-mappers-completed.png
new file mode 100644
index 000000000..38fb82006
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-completed.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-email.png b/docs/images/sso/keycloak/client-mappers-email.png
new file mode 100644
index 000000000..e1a369bab
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-email.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-empty.png b/docs/images/sso/keycloak/client-mappers-empty.png
new file mode 100644
index 000000000..01ec1d3e6
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-empty.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-id.png b/docs/images/sso/keycloak/client-mappers-id.png
new file mode 100644
index 000000000..a45638b87
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-id.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-predefined.png b/docs/images/sso/keycloak/client-mappers-predefined.png
new file mode 100644
index 000000000..750d600b7
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-predefined.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-user-property.png b/docs/images/sso/keycloak/client-mappers-user-property.png
new file mode 100644
index 000000000..c854f9521
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-user-property.png differ
diff --git a/docs/images/sso/keycloak/client-mappers-username.png b/docs/images/sso/keycloak/client-mappers-username.png
new file mode 100644
index 000000000..ff2a8fc39
Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-username.png differ
diff --git a/docs/images/sso/keycloak/client-saml-capabilities.png b/docs/images/sso/keycloak/client-saml-capabilities.png
new file mode 100644
index 000000000..a4383628a
Binary files /dev/null and b/docs/images/sso/keycloak/client-saml-capabilities.png differ
diff --git a/docs/images/sso/keycloak/client-scopes-list.png b/docs/images/sso/keycloak/client-scopes-list.png
new file mode 100644
index 000000000..16f908af4
Binary files /dev/null and b/docs/images/sso/keycloak/client-scopes-list.png differ
diff --git a/docs/images/sso/keycloak/client-signature-encryption.png b/docs/images/sso/keycloak/client-signature-encryption.png
new file mode 100644
index 000000000..b03b07a75
Binary files /dev/null and b/docs/images/sso/keycloak/client-signature-encryption.png differ
diff --git a/docs/images/sso/keycloak/clients-list.png b/docs/images/sso/keycloak/clients-list.png
new file mode 100644
index 000000000..ad05b2004
Binary files /dev/null and b/docs/images/sso/keycloak/clients-list.png differ
diff --git a/docs/images/sso/keycloak/create-client-general-settings.png b/docs/images/sso/keycloak/create-client-general-settings.png
new file mode 100644
index 000000000..866a92070
Binary files /dev/null and b/docs/images/sso/keycloak/create-client-general-settings.png differ
diff --git a/docs/images/sso/keycloak/create-client-login-settings.png b/docs/images/sso/keycloak/create-client-login-settings.png
new file mode 100644
index 000000000..6fa8b4ce4
Binary files /dev/null and b/docs/images/sso/keycloak/create-client-login-settings.png differ
diff --git a/docs/images/sso/keycloak/enable-saml.png b/docs/images/sso/keycloak/enable-saml.png
new file mode 100644
index 000000000..f66af968a
Binary files /dev/null and b/docs/images/sso/keycloak/enable-saml.png differ
diff --git a/docs/images/sso/keycloak/idp-values.png b/docs/images/sso/keycloak/idp-values.png
new file mode 100644
index 000000000..9de14f23b
Binary files /dev/null and b/docs/images/sso/keycloak/idp-values.png differ
diff --git a/docs/images/sso/keycloak/init-config.png b/docs/images/sso/keycloak/init-config.png
new file mode 100644
index 000000000..d500bb86e
Binary files /dev/null and b/docs/images/sso/keycloak/init-config.png differ
diff --git a/docs/images/sso/keycloak/org-security-section.png b/docs/images/sso/keycloak/org-security-section.png
new file mode 100644
index 000000000..bbbfb2d42
Binary files /dev/null and b/docs/images/sso/keycloak/org-security-section.png differ
diff --git a/docs/images/sso/keycloak/realm-saml-metadata.png b/docs/images/sso/keycloak/realm-saml-metadata.png
new file mode 100644
index 000000000..c5ea5d497
Binary files /dev/null and b/docs/images/sso/keycloak/realm-saml-metadata.png differ
diff --git a/docs/images/sso/keycloak/realm-settings-keys.png b/docs/images/sso/keycloak/realm-settings-keys.png
new file mode 100644
index 000000000..3add94290
Binary files /dev/null and b/docs/images/sso/keycloak/realm-settings-keys.png differ
diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx
index 95fe53ece..10caabc2f 100644
--- a/docs/integrations/cicd/githubactions.mdx
+++ b/docs/integrations/cicd/githubactions.mdx
@@ -3,17 +3,14 @@ title: "GitHub Actions"
description: "How to sync secrets from Infisical to GitHub Actions"
---
+Infisical lets you sync secrets to GitHub at the organization-level, repository-level, and repository environment-level.
+
+Prerequisites:
+- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
+- Ensure that you have admin privileges to the repository you want to sync secrets to.
+
-
- Infisical can sync secrets to GitHub repo secrets only. If your repo uses environment secrets, then stay tuned with this [issue](https://github.com/Infisical/infisical/issues/54).
-
-
- Prerequisites:
-
- - Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- - Ensure you have admin privileges to the repo you want to sync secrets to.
-
Navigate to your project's integrations tab in Infisical.
@@ -29,12 +26,27 @@ description: "How to sync secrets from Infisical to GitHub Actions"
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
-
- Select which Infisical environment secrets you want to sync to which GitHub repo and press start integration to start syncing secrets to the repo.
+
+ Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment.
+
+
+
+ 
+
+
+ 
+
+
+ 
+
+
+
+ Finally, press create integration to start syncing secrets to GitHub.

+
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
@@ -45,13 +57,13 @@ description: "How to sync secrets from Infisical to GitHub Actions"


- 
+ 
Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com`
and the **Authorization callback URL** to `https://your-domain.com/integrations/github/oauth2/callback`.
- 
-
+ 
+
If you have a GitHub organization, you can create an OAuth application under it
in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App.
@@ -59,17 +71,17 @@ description: "How to sync secrets from Infisical to GitHub Actions"
Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application.
-
- 
-
+
+ 
+
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
- `CLIENT_ID_GITHUB`: The **Client ID** of your GitHub OAuth application.
- `CLIENT_SECRET_GITHUB`: The **Client Secret** of your GitHub OAuth application.
-
+
Once added, restart your Infisical instance and use the GitHub integration.
+
-
diff --git a/docs/integrations/cloud/aws-amplify.mdx b/docs/integrations/cloud/aws-amplify.mdx
new file mode 100644
index 000000000..9edb5cf33
--- /dev/null
+++ b/docs/integrations/cloud/aws-amplify.mdx
@@ -0,0 +1,76 @@
+---
+title: "AWS Amplify"
+description: "Learn how to sync secrets from Infisical to AWS Amplify."
+---
+
+Prerequisites:
+- Infisical Cloud account
+- Add the secrets you wish to sync to Amplify to [Infisical Cloud](https://app.infisical.com)
+
+There are many approaches to sync secrets stored within Infisical to AWS Amplify. This guide describes two such approaches below.
+
+## Access Infisical secrets at Amplify build time
+
+This approach enables you to fetch secrets from Infisical during Amplify build time.
+
+
+
+ Go to your project settings in the Infisical dashboard to generate a [service token](/documentation/platform/token). This service token will allow you to authenticate and fetch secrets from Infisical. Once you have created a service token with the required permissions, youโll need to provide the token to the CLI installed in your Docker container.
+
+
+ 
+ 1. In the Amplify console, choose App Settings, and then select Environment variables.
+ 2. In the Environment variables section, select Manage variables.
+ 3. Under Variable, enter the key **INFISICAL_TOKEN**. For the value, enter the generated service token from the previous step.
+ 4. Click save.
+
+
+ In the prebuild phase, add the command in AWS Amplify to install the Infisical CLI.
+
+ ```yaml
+ build:
+ phases:
+ preBuild:
+ commands:
+ - sudo curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.rpm.sh' | sudo -E bash
+ - sudo yum install infisical
+ ```
+
+
+ You can now pull secrets from Infisical using the CLI and save them as a `.env` file. To do this, modify the build commands.
+
+ ```yaml
+ build:
+ phases:
+ build:
+ commands:
+ - INFISICAL_TOKEN=${INFISICAL_TOKEN}
+ - infisical export --format=dotenv > .env
+ -
+ ```
+
+
+
+## Sync Secrets Using AWS SSM Parameter Store
+
+Another approach to use secrets from Infisical in AWS Amplify is to utilize AWS Parameter Store.
+At high level, you begin by using Infisical's AWS SSM Parameter Store integration to sync secrets from Infisical to AWS SSM Parameter Store. You then instruct AWS Amplify to consume those secrets from AWS SSM Parameter Store as [environment secrets](https://docs.aws.amazon.com/amplify/latest/userguide/environment-variables.html#environment-secrets).
+
+
+
+ Follow the [Infisical AWS SSM Parameter Store Integration Guide](./aws-parameter-store) to set up the integration. Pause once you reach the step where it asks you to select the path you would like to sync.
+
+
+ 
+ 1. Open your AWS Amplify App console.
+ 2. Go to **Actions >> View App Settings**
+ 3. The App ID will be the last part of the App ARN field after the slash.
+
+
+ You need to set the path in the format `/amplify/[amplify_app_id]/[your-amplify-environment-name]` as the path option in AWS SSM Parameter Infisical Integration.
+
+
+
+
+ Accessing an environment secret during a build is similar to accessing environment variables, except that environment secrets are stored in `process.env.secrets` as a JSON string.
+
diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx
index 6fd341018..c872e39f0 100644
--- a/docs/integrations/cloud/aws-parameter-store.mdx
+++ b/docs/integrations/cloud/aws-parameter-store.mdx
@@ -1,6 +1,6 @@
---
title: "AWS Parameter Store"
-description: "How to sync secrets from Infisical to AWS Parameter Store"
+description: "Learn how to sync secrets from Infisical to AWS Parameter Store."
---
Prerequisites:
@@ -29,7 +29,8 @@ Prerequisites:
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParametersByPath",
- "ssm:DeleteParameters"
+ "ssm:DeleteParameters",
+ "ssm:AddTagsToResource" // if you need to add tags to secrets
],
"Resource": "*"
}
diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx
index f761d5164..2ab45c620 100644
--- a/docs/integrations/cloud/aws-secret-manager.mdx
+++ b/docs/integrations/cloud/aws-secret-manager.mdx
@@ -1,6 +1,6 @@
---
title: "AWS Secrets Manager"
-description: "How to sync secrets from Infisical to AWS Secrets Manager"
+description: "Learn how to sync secrets from Infisical to AWS Secrets Manager."
---
Prerequisites:
@@ -28,13 +28,17 @@ Prerequisites:
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret",
- "secretsmanager:UpdateSecret"
+ "secretsmanager:UpdateSecret",
+ "secretsmanager:TagResource", // if you need to add tags to secrets
+ "kms:ListKeys", // if you need to specify the KMS key
+ "kms:ListAliases" // if you need to specify the KMS key
],
"Resource": "*"
}
]
}
```
+
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
@@ -42,7 +46,7 @@ Prerequisites:



-
+
Navigate to your project's integrations tab in Infisical.

@@ -51,23 +55,46 @@ Prerequisites:

-
- If this is your project's first cloud integration, then you'll have to grant
- Infisical access to your project's environment variables. Although this step
- breaks E2EE, it's necessary for Infisical to sync the environment variables to
- the cloud platform.
-
- Select which Infisical environment secrets you want to sync to which AWS Secrets Manager region and under which secret name. Then, press create integration to start syncing secrets to AWS Secrets Manager.
+ Select how you want to integration to work by specifying a number of parameters:
+
+
+ The environment in Infisical from which you want to sync secrets to AWS Secrets Manager.
+
+
+ The path within the preselected environment form which you want to sync secrets to AWS Secrets Manager.
+
+
+ The region that you want to integrate with in AWS Secrets Manager.
+
+
+ The secret name/path in AWS into which you want to sync the secrets from Infisical.
+

+ Optionally, you can add tags or specify the encryption key of all the secrets created via this integration:
+
+
+ The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API.
+
+
+ The alias/ID of the AWS KMS key used for encryption. Please note that key should be enabled in order to work and the IAM user should have access to it.
+
+ 
+
+ Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager.
+
Infisical currently syncs environment variables to AWS Secrets Manager as
key-value pairs under one secret. We're actively exploring ways to help users
group environment variable key-pairs under multiple secrets for greater
control.
+
+ Please note that upon deleting secrets in Infisical, AWS Secrets Manager immediately makes the secrets inaccessible but only schedules them for deletion after at least 7 days.
+
+
-
\ No newline at end of file
+
diff --git a/docs/integrations/frameworks/terraform.mdx b/docs/integrations/frameworks/terraform.mdx
index 2643ca5af..7d30ec0d9 100644
--- a/docs/integrations/frameworks/terraform.mdx
+++ b/docs/integrations/frameworks/terraform.mdx
@@ -1,6 +1,6 @@
---
title: "Terraform"
-description: "Fetch Secrets From Infisical With Terraform"
+description: "Learn how to fetch Secrets From Infisical With Terraform."
---
This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform.
diff --git a/docs/integrations/platforms/ansible.mdx b/docs/integrations/platforms/ansible.mdx
index 2d524d55c..ad95d0d5d 100644
--- a/docs/integrations/platforms/ansible.mdx
+++ b/docs/integrations/platforms/ansible.mdx
@@ -1,6 +1,6 @@
---
title: "Ansible"
-description: "How to use Infisical for secret management in Ansible"
+description: "Learn how to use Infisical for secret management in Ansible."
---
The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/).
diff --git a/docs/integrations/platforms/docker-compose.mdx b/docs/integrations/platforms/docker-compose.mdx
index 238e3dd71..1c061e04e 100644
--- a/docs/integrations/platforms/docker-compose.mdx
+++ b/docs/integrations/platforms/docker-compose.mdx
@@ -1,6 +1,6 @@
---
title: "Docker Compose"
-description: "How to use Infisical to inject environment variables into services defined in your Docker Compose file."
+description: "Find out how to use Infisical to inject environment variables into services defined in your Docker Compose file."
---
Prerequisites:
diff --git a/docs/integrations/platforms/docker-intro.mdx b/docs/integrations/platforms/docker-intro.mdx
index 2823fc8ca..bec0f4213 100644
--- a/docs/integrations/platforms/docker-intro.mdx
+++ b/docs/integrations/platforms/docker-intro.mdx
@@ -1,6 +1,6 @@
---
title: "Docker"
-description: "Learn how to feed secrets from Infisical into your Docker application"
+description: "Learn how to feed secrets from Infisical into your Docker application."
---
There are many methods to inject Infisical secrets into Docker-based applications.
Regardless of the method you choose, they all inject secrets from Infisical as environment variables into your Docker container.
diff --git a/docs/integrations/platforms/docker-pass-envs.mdx b/docs/integrations/platforms/docker-pass-envs.mdx
index d6451de71..04cc36d6b 100644
--- a/docs/integrations/platforms/docker-pass-envs.mdx
+++ b/docs/integrations/platforms/docker-pass-envs.mdx
@@ -1,6 +1,6 @@
---
title: "Docker Run"
-description: "Pass secrets to your docker container at run time"
+description: "Learn how to pass secrets to your docker container at run time."
---
This method allows you to feed secrets from Infisical into your container using the `--env-file` flag of `docker run` command.
diff --git a/docs/infisical-agent/guides/docker-swarm-with-agent.mdx b/docs/integrations/platforms/docker-swarm-with-agent.mdx
similarity index 98%
rename from docs/infisical-agent/guides/docker-swarm-with-agent.mdx
rename to docs/integrations/platforms/docker-swarm-with-agent.mdx
index 8ab4ca962..30118a8f0 100644
--- a/docs/infisical-agent/guides/docker-swarm-with-agent.mdx
+++ b/docs/integrations/platforms/docker-swarm-with-agent.mdx
@@ -1,6 +1,6 @@
---
title: 'Docker Swarm'
-description: "How to manage secrets in Docker Swarm services"
+description: "Learn how to manage secrets in Docker Swarm services."
---
In this guide, we'll demonstrate how to use Infisical for managing secrets within Docker Swarm.
diff --git a/docs/integrations/platforms/docker.mdx b/docs/integrations/platforms/docker.mdx
index 8fbf288ad..92bd57943 100644
--- a/docs/integrations/platforms/docker.mdx
+++ b/docs/integrations/platforms/docker.mdx
@@ -1,6 +1,6 @@
---
title: "Docker Entrypoint"
-description: "How to use Infisical to inject environment variables into a Docker container."
+description: "Learn how to use Infisical to inject environment variables into a Docker container."
---
This approach allows you to inject secrets from Infisical directly into your application.
diff --git a/docs/integrations/platforms/ecs-with-agent.mdx b/docs/integrations/platforms/ecs-with-agent.mdx
index bbb88ea64..43760d2bb 100644
--- a/docs/integrations/platforms/ecs-with-agent.mdx
+++ b/docs/integrations/platforms/ecs-with-agent.mdx
@@ -1,6 +1,6 @@
---
title: 'Amazon ECS'
-description: "How to deliver secrets to Amazon Elastic Container Service"
+description: "Learn how to deliver secrets to Amazon Elastic Container Service."
---

diff --git a/docs/infisical-agent/overview.mdx b/docs/integrations/platforms/infisical-agent.mdx
similarity index 97%
rename from docs/infisical-agent/overview.mdx
rename to docs/integrations/platforms/infisical-agent.mdx
index 9265d9bfe..1516ae045 100644
--- a/docs/infisical-agent/overview.mdx
+++ b/docs/integrations/platforms/infisical-agent.mdx
@@ -1,12 +1,12 @@
---
-title: "Overview"
+title: "Infisical Agent"
description: "This page describes how to manage secrets using Infisical Agent."
---
Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical.
It eliminates the need to modify application logic by enabling clients to decide how they want their secrets rendered through the use of templates.
-
+
### Key features:
- Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume
@@ -52,7 +52,7 @@ While specifying an authentication method is mandatory to start the agent, confi
| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. |
| `templates[].source-path` | The path to the template file that should be used to render secrets. |
| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. |
-| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `60s` (optional) |
+| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) |
| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) |
| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) |
diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx
index 7ddb616b2..6dbe4acde 100644
--- a/docs/integrations/platforms/kubernetes.mdx
+++ b/docs/integrations/platforms/kubernetes.mdx
@@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme
## Install Operator
-The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
+The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
@@ -61,23 +61,39 @@ Once you have installed the operator to your cluster, you'll need to create a `I
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret
metadata:
- # Name of of this InfisicalSecret resource
- name: infisicalsecret-sample
+ name: infisicalsecret-sample
+ labels:
+ label-to-be-passed-to-managed-secret: sample-value
+ annotations:
+ example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
spec:
- # The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
- hostAPI: https://app.infisical.com/api
- resyncInterval: 60
- authentication:
- serviceToken:
- serviceTokenSecretReference:
- secretName: service-token
+ hostAPI: https://app.infisical.com/api
+ resyncInterval: 10
+ authentication:
+ # Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
+ # If you have multiple authentication methods defined, it may cause issues.
+ universalAuth:
+ secretsScope:
+ projectSlug:
+ envSlug: # "dev", "staging", "prod", etc..
+ secretsPath: "" # Root is "/"
+ credentialsRef:
+ secretName: universal-auth-credentials
+ secretNamespace: default
+
+ serviceToken:
+ serviceTokenSecretReference:
+ secretName: service-token
+ secretNamespace: default
+ secretsScope:
+ envSlug:
+ secretsPath: # Root is "/"
+
+ managedSecretReference:
+ secretName: managed-secret
secretNamespace: default
- secretsScope:
- envSlug: dev
- secretsPath: "/"
- managedSecretReference:
- secretName: managed-secret # <-- the name of kubernetes secret that will be created
- secretNamespace: default # <-- where the kubernetes secret should be created
+ creationPolicy: "Orphan" ## Owner | Orphan (default)
+ # secretType: kubernetes.io/dockerconfigjson
```
### InfisicalSecret CRD properties
@@ -105,11 +121,60 @@ Default re-sync interval is every 1 minute.
- This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched. Currently, only [Service Tokens](../../documentation/platform/token) can be used to authenticate with Infisical.
+ This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched
-
- The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and name space of secret that stores this service token.
+
+ The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials.
+
+
+
+ You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth).
+
+
+ Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials.
+ To quickly create a Kubernetes secret containing the identity credentials, you can run the command below.
+
+ Make sure you replace `` with the identity client ID and `` with the identity client secret.
+
+ ``` bash
+ kubectl create secret generic universal-auth-credentials --from-literal=clientId="" --from-literal=clientSecret=""
+ ```
+
+
+
+ Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource.
+
+
+
+
+
+
+ Make sure to also populate the `secretsScope` field with the project slug _`projectSlug`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
+
+
+ ## Example
+ ```yaml
+ apiVersion: secrets.infisical.com/v1alpha1
+ kind: InfisicalSecret
+ metadata:
+ name: infisicalsecret-sample-crd
+ spec:
+ authentication:
+ universalAuth:
+ secretsScope:
+ projectSlug: # <-- project slug
+ envSlug: # "dev", "staging", "prod", etc..
+ secretsPath: "" # Root is "/"
+ credentialsRef:
+ secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials
+ secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials
+ ...
+ ```
+
+
+
+ The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token.
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
#### 1. Generate service token
@@ -122,13 +187,17 @@ Default re-sync interval is every 1 minute.
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `` with your service token.
``` bash
- kubectl create secret generic service-token --from-literal=infisicalToken=
+ kubectl create secret generic service-token --from-literal=infisicalToken=""
```
#### 3. Add reference for the Kubernetes secret containing service token
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
+
+ Make sure to also populate the `secretsScope` field with the, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
+
+
## Example
```yaml
apiVersion: secrets.infisical.com/v1alpha1
@@ -141,25 +210,13 @@ Default re-sync interval is every 1 minute.
serviceTokenSecretReference:
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
+ secretsScope:
+ envSlug: # "dev", "staging", "prod", etc..
+ secretsPath: # Root is "/"
...
```
-
- This block defines the scope of what secrets should be fetched. This is needed as your service token can have access to multiple folders and environments.
- A scope is defined by `envSlug` and `secretsPath`.
-
- #### envSlug
-
- This refers to the short hand name of an environment. For example for the `development` environment the environment slug is `dev`. You can locate the slug of your environment by heading to your project settings in the Infisical dashboard.
-
- #### secretsPath
-
- secretsPath is the path to the secret in the given environment. For example a path of `/` would refer to the root of the environment whereas `/folder1` would refer to the secrets in folder1 from the root.
-
- Both fields are required.
-
-
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
@@ -176,6 +233,19 @@ The namespace of the managed Kubernetes secret to be created.
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
+
+
+Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
+This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
+
+#### Available options
+- `Orphan` (default)
+- `Owner`
+
+
+ When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in the same namespace as where the managed kubernetes secret.
+
+
### Propagating labels & annotations
diff --git a/docs/internals/components.mdx b/docs/internals/components.mdx
index 29522b0bb..65506a500 100644
--- a/docs/internals/components.mdx
+++ b/docs/internals/components.mdx
@@ -1,6 +1,6 @@
---
title: "Components"
-description: "Infisical's components span multiple clients, an API, and a storage backend"
+description: "Infisical's components span multiple clients, an API, and a storage backend."
---
## Infisical API
diff --git a/docs/internals/flows.mdx b/docs/internals/flows.mdx
index e18a37a31..0da671d64 100644
--- a/docs/internals/flows.mdx
+++ b/docs/internals/flows.mdx
@@ -1,6 +1,6 @@
---
title: "Flows"
-description: "Infisical's core flows have strong cryptographic underpinnings"
+description: "Infisical's core flows have strong cryptographic underpinnings."
---
## Signup
diff --git a/docs/internals/overview.mdx b/docs/internals/overview.mdx
index e5c47682d..e64327a03 100644
--- a/docs/internals/overview.mdx
+++ b/docs/internals/overview.mdx
@@ -1,6 +1,6 @@
---
title: "Overview"
-description: "How Infisical works under the hood"
+description: "Read how Infisical works under the hood."
---
This section covers the internals of Infisical including its technical underpinnings, architecture, and security properties.
@@ -12,26 +12,26 @@ This section covers the internals of Infisical including its technical underpinn
## Learn More
-
- Learn about the fundamental parts of Infisical
+
+ Learn about the fundamental parts of Infisical.
-
- Find out more about the structure of core user flows in Infisical
+
+ Find out more about the structure of core user flows in Infisical.
- Read about most common security-related topics and questions
+ Read about most common security-related topics and questions.
- Learn best practices for utilizing Infisical service tokens
+ Learn best practices for utilizing Infisical service tokens.
diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx
index 180d44ec4..1b0fb9f32 100644
--- a/docs/internals/security.mdx
+++ b/docs/internals/security.mdx
@@ -1,6 +1,6 @@
---
title: "Security"
-description: "Infisical's security model includes many considerations and initiatives"
+description: "Infisical's security model includes many considerations and initiatives."
---
Given that Infisical is a secret management platform that manages sensitive data, the Infisical security model is very important.
@@ -87,13 +87,23 @@ Since these encryption operations occur on the client-side, the Infisical API is
### High availability
-Infisical leverages the robust container orchestration capabilities of Kubernetes and the inherent high availability features of the storage backend (i.e. Bitnami MongoDB) to ensure resilience and fault tolerance.
+Infisical Cloud utilizes several strategies to ensure high availability, leveraging AWS services to maintain continuous operation and data integrity.
-- Kubernetes: By deploying multiple replicas of Infisical application on Kubernetes, operations continue even if a single instance fails. Kubernetes Services facilitate load balancing, effectively distributing traffic across your applicationโs instances and ensuring optimal performance.
-- Storage backend: Bitnami MongoDB supports replica sets, which provide data redundancy and automatic failover for the underlying database.
-- If using [Infisical Cloud](https://app.infisical.com), data is stored in a Mongo Atlas cluster with storage autoscaling and cluster tier autoscaling enabled; as you'd expect, the cluster sits on a dedicated node.
+#### Multi-AZ AWS RDS
+Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments.
+This configuration ensures that the database service is highly available and durable.
+AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ).
+This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption.
+The continuous backup and replication to the standby instance safeguard data against loss and ensure its availability even during system failures.
-Together, Kubernetesโ self-healing mechanisms and Bitnami MongoDBโs failover capabilities work to create a highly available and fault-tolerant application capable of recovering gracefully from unexpected failures.
+#### Multi-AZ ECS for Container Orchestration
+Infisical Cloud leverages Amazon Elastic Container Service (ECS) in a Multi-AZ configuration for container orchestration.
+This arrangement enables the management and operation of containers across multiple availability zones, increasing the application's fault tolerance.
+Should there be an AZ failure, load is seamlessly sent to an operational AZ, thus minimizing downtime and preserving service availability.
+
+#### Standby Regions for Regional Failover
+To fight regional outages, secondary regions are always in standby mode and maintained with up-to-date configurations and data, ready to take over in case the primary region fails.
+The standby regions enable a rapid transition and service continuity with minimal disruption in the event of a complete regional failure, ensuring that Infisical Cloud services remain accessible.
### Snapshots
diff --git a/docs/internals/service-tokens.mdx b/docs/internals/service-tokens.mdx
index a04eccee7..3222b1aa8 100644
--- a/docs/internals/service-tokens.mdx
+++ b/docs/internals/service-tokens.mdx
@@ -1,6 +1,6 @@
---
title: "Service tokens"
-description: "Understanding service tokens and their best practices"
+description: "Understanding service tokens and their best practices."
---
โ
Many clients use service tokens to authenticate and read/write secrets from/to Infisical; they can be created in your project settings.
diff --git a/docs/mint.json b/docs/mint.json
index 74b56f3b0..94654ebe3 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -8,19 +8,24 @@
},
"favicon": "/favicon.png",
"colors": {
- "primary": "#A1B659",
- "light": "#E1EB55",
+ "primary": "#26272b",
+ "light": "#97b31d",
"dark": "#A1B659",
- "ultraLight": "#EFF4DD",
+ "ultraLight": "#E7F256",
"ultraDark": "#8D9F4C",
"background": {
+ "light": "#ffffff",
"dark": "#0D1117"
},
"anchors": {
- "from": "#A1B659",
- "to": "#F8B7BD"
+ "from": "#000000",
+ "to": "#707174"
}
},
+ "modeToggle": {
+ "default": "light",
+ "isHidden": true
+ },
"feedback": {
"suggestEdit": true,
"raiseIssue": true,
@@ -39,61 +44,39 @@
"name": "Start for Free",
"url": "https://app.infisical.com/signup"
},
- "anchors": [
+ "tabs": [
{
- "name": "Internals",
- "icon": "sitemap",
- "url": "internals"
+ "name": "Integrations",
+ "url": "integrations"
},
{
- "name": "SDKs",
- "icon": "puzzle-piece",
- "url": "sdks"
+ "name": "CLI",
+ "url": "cli"
},
{
"name": "API Reference",
- "icon": "cloud",
"url": "api-reference"
},
+ {
+ "name": "SDKs",
+ "url": "sdks"
+ },
{
"name": "Changelog",
- "icon": "timer",
"url": "changelog"
- },
- {
- "name": "Contributing",
- "icon": "code",
- "url": "contributing"
- },
- {
- "name": "Blog",
- "icon": "newspaper",
- "url": "https://infisical.com/blog"
- },
- {
- "name": "Slack",
- "icon": "slack",
- "url": "https://infisical.com/slack"
- },
- {
- "name": "GitHub",
- "icon": "github",
- "url": "https://github.com/Infisical/infisical"
}
],
"navigation": [
{
- "group": "Overview",
+ "group": "Getting Started",
"pages": [
+ "documentation/getting-started/introduction",
{
- "group": "Getting Started",
+ "group": "Quickstart",
"pages": [
- "documentation/getting-started/introduction",
- "documentation/getting-started/platform",
- "documentation/getting-started/sdks",
- "integrations/platforms/kubernetes",
- "integrations/platforms/docker-intro",
- "documentation/getting-started/api"
+ "documentation/guides/local-development",
+ "documentation/guides/staging",
+ "documentation/guides/production"
]
},
{
@@ -102,7 +85,8 @@
"documentation/guides/introduction",
"documentation/guides/node",
"documentation/guides/python",
- "documentation/guides/nextjs-vercel"
+ "documentation/guides/nextjs-vercel",
+ "documentation/guides/microsoft-power-apps"
]
}
]
@@ -113,21 +97,35 @@
"documentation/platform/organization",
"documentation/platform/project",
"documentation/platform/folder",
- "documentation/platform/secret-reference",
- "documentation/platform/webhooks",
- "documentation/platform/pit-recovery",
- "documentation/platform/audit-logs",
+ {
+ "group": "Secrets",
+ "pages": [
+ "documentation/platform/secret-versioning",
+ "documentation/platform/pit-recovery",
+ "documentation/platform/secret-reference",
+ "documentation/platform/webhooks"
+ ]
+ },
{
"group": "Identities",
"pages": [
"documentation/platform/identities/overview",
- "documentation/platform/identities/universal-auth"
+ "documentation/platform/identities/user-identities",
+ "documentation/platform/identities/machine-identities"
+ ]
+ },
+ {
+ "group": "Access Control",
+ "pages": [
+ "documentation/platform/access-controls/overview",
+ "documentation/platform/access-controls/role-based-access-controls",
+ "documentation/platform/access-controls/additional-privileges",
+ "documentation/platform/access-controls/temporary-access",
+ "documentation/platform/access-controls/access-requests",
+ "documentation/platform/pr-workflows",
+ "documentation/platform/audit-logs"
]
},
- "documentation/platform/token",
- "documentation/platform/mfa",
- "documentation/platform/pr-workflows",
- "documentation/platform/role-based-access-controls",
{
"group": "Secret Rotation",
"pages": [
@@ -138,6 +136,25 @@
"documentation/platform/secret-rotation/aws-iam"
]
},
+ {
+ "group": "Dynamic Secrets",
+ "pages": [
+ "documentation/platform/dynamic-secrets/overview",
+ "documentation/platform/dynamic-secrets/postgresql",
+ "documentation/platform/dynamic-secrets/mysql",
+ "documentation/platform/dynamic-secrets/oracle"
+ ]
+ },
+ "documentation/platform/groups"
+ ]
+ },
+ {
+ "group": "Authentication Methods",
+ "pages": [
+ "documentation/platform/auth-methods/email-password",
+ "documentation/platform/token",
+ "documentation/platform/identities/universal-auth",
+ "documentation/platform/mfa",
{
"group": "SSO",
"pages": [
@@ -148,6 +165,7 @@
"documentation/platform/sso/okta",
"documentation/platform/sso/azure",
"documentation/platform/sso/jumpcloud",
+ "documentation/platform/sso/keycloak-saml",
"documentation/platform/sso/google-saml"
]
},
@@ -191,6 +209,14 @@
"self-hosting/guides/mongo-to-postgres"
]
},
+ {
+ "group": "Reference architectures",
+ "pages": [
+ "self-hosting/reference-architectures/aws-ecs",
+ "self-hosting/reference-architectures/on-premise"
+ ]
+ },
+ "self-hosting/ee",
"self-hosting/faq"
]
},
@@ -207,6 +233,7 @@
"cli/commands/run",
"cli/commands/secrets",
"cli/commands/export",
+ "cli/commands/token",
"cli/commands/service-token",
"cli/commands/vault",
"cli/commands/user",
@@ -226,19 +253,6 @@
"cli/faq"
]
},
- {
- "group": "Agent",
- "pages": [
- "infisical-agent/overview",
- {
- "group": "Use cases",
- "pages": [
- "infisical-agent/guides/docker-swarm-with-agent",
- "integrations/platforms/ecs-with-agent"
- ]
- }
- ]
- },
{
"group": "Infrastructure Integrations",
"pages": [
@@ -246,10 +260,11 @@
"group": "Container orchestrators",
"pages": [
"integrations/platforms/kubernetes",
- "infisical-agent/guides/docker-swarm-with-agent",
+ "integrations/platforms/docker-swarm-with-agent",
"integrations/platforms/ecs-with-agent"
]
},
+ "integrations/platforms/infisical-agent",
{
"group": "Docker",
"pages": [
@@ -270,22 +285,27 @@
"group": "AWS",
"pages": [
"integrations/cloud/aws-parameter-store",
- "integrations/cloud/aws-secret-manager"
+ "integrations/cloud/aws-secret-manager",
+ "integrations/cloud/aws-amplify"
]
},
- {
- "group": "Digital Ocean",
- "pages": ["integrations/cloud/digital-ocean-app-platform"]
- },
"integrations/cloud/vercel",
"integrations/cloud/azure-key-vault",
"integrations/cloud/gcp-secret-manager",
+ {
+ "group": "Cloudflare",
+ "pages": [
+ "integrations/cloud/cloudflare-pages",
+ "integrations/cloud/cloudflare-workers"
+ ]
+ },
+ "integrations/cloud/heroku",
+ "integrations/cloud/render",
{
"group": "View more",
"pages": [
- "integrations/cloud/heroku",
+ "integrations/cloud/digital-ocean-app-platform",
"integrations/cloud/netlify",
- "integrations/cloud/render",
"integrations/cloud/railway",
"integrations/cloud/flyio",
"integrations/cloud/laravel-forge",
@@ -293,8 +313,6 @@
"integrations/cloud/northflank",
"integrations/cloud/hasura-cloud",
"integrations/cloud/terraform-cloud",
- "integrations/cloud/cloudflare-pages",
- "integrations/cloud/cloudflare-workers",
"integrations/cloud/qovery",
"integrations/cloud/hashicorp-vault",
"integrations/cloud/cloud-66",
@@ -306,17 +324,17 @@
{
"group": "CI/CD Integrations",
"pages": [
- "integrations/cloud/teamcity",
+ "integrations/cicd/jenkins",
"integrations/cicd/githubactions",
"integrations/cicd/gitlab",
+ "integrations/cicd/bitbucket",
+ "integrations/cloud/teamcity",
{
"group": "View more",
"pages": [
"integrations/cicd/circleci",
"integrations/cicd/travisci",
- "integrations/cicd/bitbucket",
"integrations/cicd/codefresh",
- "integrations/cicd/jenkins",
"integrations/cloud/checkly"
]
}
@@ -355,9 +373,18 @@
"pages": ["integrations/build-tools/gradle"]
},
{
- "group": "Overview",
+ "group": "",
"pages": ["sdks/overview"]
},
+ {
+ "group": "SDK's",
+ "pages": [
+ "sdks/languages/node",
+ "sdks/languages/python",
+ "sdks/languages/java",
+ "sdks/languages/csharp"
+ ]
+ },
{
"group": "Overview",
"pages": [
@@ -365,24 +392,13 @@
"api-reference/overview/authentication",
{
"group": "Examples",
- "pages": [
- "api-reference/overview/examples/note",
- "api-reference/overview/examples/e2ee-disabled",
- "api-reference/overview/examples/e2ee-enabled"
- ]
+ "pages": ["api-reference/overview/examples/integration"]
}
]
},
{
"group": "Endpoints",
"pages": [
- {
- "group": "Users",
- "pages": [
- "api-reference/endpoints/users/me",
- "api-reference/endpoints/users/my-organizations"
- ]
- },
{
"group": "Identities",
"pages": [
@@ -428,7 +444,6 @@
"api-reference/endpoints/workspaces/list-identity-memberships",
"api-reference/endpoints/workspaces/update-identity-membership",
"api-reference/endpoints/workspaces/delete-identity-membership",
- "api-reference/endpoints/workspaces/workspace-key",
"api-reference/endpoints/workspaces/secret-snapshots",
"api-reference/endpoints/workspaces/rollback-snapshot"
]
@@ -450,6 +465,14 @@
"api-reference/endpoints/folders/delete"
]
},
+ {
+ "group": "Secret Tags",
+ "pages": [
+ "api-reference/endpoints/secret-tags/list",
+ "api-reference/endpoints/secret-tags/create",
+ "api-reference/endpoints/secret-tags/delete"
+ ]
+ },
{
"group": "Secrets",
"pages": [
@@ -457,11 +480,13 @@
"api-reference/endpoints/secrets/create",
"api-reference/endpoints/secrets/read",
"api-reference/endpoints/secrets/update",
- "api-reference/endpoints/secrets/delete"
+ "api-reference/endpoints/secrets/delete",
+ "api-reference/endpoints/secrets/attach-tags",
+ "api-reference/endpoints/secrets/detach-tags"
]
},
{
- "group": "Secret imports",
+ "group": "Secret Imports",
"pages": [
"api-reference/endpoints/secret-imports/list",
"api-reference/endpoints/secret-imports/create",
@@ -469,6 +494,31 @@
"api-reference/endpoints/secret-imports/delete"
]
},
+ {
+ "group": "Identity Specific Privilege",
+ "pages": [
+ "api-reference/endpoints/identity-specific-privilege/create-permanent",
+ "api-reference/endpoints/identity-specific-privilege/create-temporary",
+ "api-reference/endpoints/identity-specific-privilege/update",
+ "api-reference/endpoints/identity-specific-privilege/delete",
+ "api-reference/endpoints/identity-specific-privilege/find-by-slug",
+ "api-reference/endpoints/identity-specific-privilege/list"
+ ]
+ },
+ {
+ "group": "Integrations",
+ "pages": [
+ "api-reference/endpoints/integrations/create-auth",
+ "api-reference/endpoints/integrations/list-auth",
+ "api-reference/endpoints/integrations/find-auth",
+ "api-reference/endpoints/integrations/delete-auth",
+ "api-reference/endpoints/integrations/delete-auth-by-id",
+ "api-reference/endpoints/integrations/create",
+ "api-reference/endpoints/integrations/update",
+ "api-reference/endpoints/integrations/delete",
+ "api-reference/endpoints/integrations/list-project-integrations"
+ ]
+ },
{
"group": "Service Tokens",
"pages": ["api-reference/endpoints/service-tokens/get"]
@@ -490,22 +540,21 @@
]
},
{
- "group": "Overview",
+ "group": "",
"pages": ["changelog/overview"]
},
{
- "group": "",
+ "group": "Contributing",
"pages": [
- {
- "group": "Getting Started",
- "pages": [
- "contributing/getting-started/overview",
- "contributing/getting-started/code-of-conduct",
- "contributing/getting-started/pull-requests",
- "contributing/getting-started/faq"
-
- ]
- },
+ {
+ "group": "Getting Started",
+ "pages": [
+ "contributing/getting-started/overview",
+ "contributing/getting-started/code-of-conduct",
+ "contributing/getting-started/pull-requests",
+ "contributing/getting-started/faq"
+ ]
+ },
{
"group": "Contributing to platform",
"pages": [
@@ -515,11 +564,9 @@
]
},
{
- "group": "Contributing to SDK",
- "pages": [
- "contributing/sdk/developing"
- ]
- }
+ "group": "Contributing to SDK",
+ "pages": ["contributing/sdk/developing"]
+ }
]
}
],
diff --git a/docs/sdks/languages/csharp.mdx b/docs/sdks/languages/csharp.mdx
index ecfd67c27..b3a1d2086 100644
--- a/docs/sdks/languages/csharp.mdx
+++ b/docs/sdks/languages/csharp.mdx
@@ -1,6 +1,7 @@
---
title: "Infisical .NET SDK"
-icon: "C#"
+sidebarTitle: ".NET"
+icon: "bars"
---
If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application.
diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx
index 40d577926..5b8797b5d 100644
--- a/docs/sdks/languages/java.mdx
+++ b/docs/sdks/languages/java.mdx
@@ -1,5 +1,6 @@
---
title: "Infisical Java SDK"
+sidebarTitle: "Java"
icon: "java"
---
diff --git a/docs/sdks/languages/node.mdx b/docs/sdks/languages/node.mdx
index 7712caaf1..4816392ed 100644
--- a/docs/sdks/languages/node.mdx
+++ b/docs/sdks/languages/node.mdx
@@ -1,5 +1,6 @@
---
title: "Infisical Node.js SDK"
+sidebarTitle: "Node.js"
icon: "node"
---
diff --git a/docs/sdks/languages/python.mdx b/docs/sdks/languages/python.mdx
index da92b1a6e..0ce221757 100644
--- a/docs/sdks/languages/python.mdx
+++ b/docs/sdks/languages/python.mdx
@@ -1,5 +1,6 @@
---
title: "Infisical Python SDK"
+sidebarTitle: "Python"
icon: "python"
---
diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx
index d032311f2..578e8ad0f 100644
--- a/docs/sdks/overview.mdx
+++ b/docs/sdks/overview.mdx
@@ -1,5 +1,6 @@
---
-title: "Introduction"
+title: "SDKs"
+sidebarTitle: "Introduction"
---
From local development to production, Infisical SDKs provide the easiest way for your app to fetch back secrets from Infisical on demand.
diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx
index 4bb56ebcd..4c1456d3b 100644
--- a/docs/self-hosting/configuration/envars.mdx
+++ b/docs/self-hosting/configuration/envars.mdx
@@ -1,6 +1,6 @@
---
title: "Configurations"
-description: "Configure environment variables for self-hosted Infisical"
+description: "Read how to configure environment variables for self-hosted Infisical."
---
@@ -121,24 +121,35 @@ Without email configuration, Infisical's core functions like sign-up/login and s
- 1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console.
- 2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials
+
+
+ This will be used to verify the email you are sending from.
+ 
+
+ If you AWS SES is under sandbox mode, you will only be able to send emails to verified identies.
+
+
+
+ Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials
- 
+ 
- 
+ 
+
+
+ With your AWS SES SMTP credentials, you can now set up your SMTP environment variables for your Infisical instance.
- 3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables:
-
- ```
- SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
- SMTP_USERNAME=xxx # your SMTP username
- SMTP_PASSWORD=xxx # your SMTP password
- SMTP_PORT=587
- SMTP_SECURE=true
- SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
- SMTP_FROM_NAME=Infisical
- ```
+ ```
+ SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
+ SMTP_USERNAME=xxx # your SMTP username
+ SMTP_PASSWORD=xxx # your SMTP password
+ SMTP_PORT=465
+ SMTP_SECURE=true
+ SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
+ SMTP_FROM_NAME=Infisical
+ ```
+
+
Remember that you will need to restart Infisical for this to work properly.
@@ -335,6 +346,10 @@ To login into Infisical with OAuth providers such as Google, configure the assoc
Requires enterprise license. Please contact team@infisical.com to get more information.
+
+ Configure SAML organization slug to automatically redirect all users of your Infisical instance to the identity provider.
+
+
diff --git a/docs/self-hosting/configuration/requirements.mdx b/docs/self-hosting/configuration/requirements.mdx
index 262c7fb7c..2e31ac853 100644
--- a/docs/self-hosting/configuration/requirements.mdx
+++ b/docs/self-hosting/configuration/requirements.mdx
@@ -1,6 +1,6 @@
---
title: "Requirements"
-description: ""
+description: "Find out the minimal requirements for operating Infisical."
---
This page details the minimum requirements necessary for installing and using Infisical.
@@ -47,7 +47,7 @@ Recommended minimum memory hardware for different sizes of deployments:
PostgreSQL is the only database supported by Infisical. Infisical has been extensively tested with Postgres version 16. We recommend using versions 14 and up for optimal compatibility.
Recommended resource allocation based on deployment size:
-- **small:**ย 1 vCPU / 2 GB RAM / 10 GB Disk
+- **small:**ย 2 vCPU / 8 GB RAM / 20 GB Disk
- **large:** 4vCPU / 16 GB RAM / 100 GB Disk
### Redis
@@ -58,7 +58,7 @@ Redis requirements:
- Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2.
- Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA).
-- Redis storage needs are minimal: a setup with 1 vCPU, 1 GB RAM, and 1GB SSD will be sufficient for small deployments.
+- Redis storage needs are minimal: a setup with 2 vCPU, 4 GB RAM, and 30GB SSD will be sufficient for small deployments.
## Supported Web Browsers
diff --git a/docs/self-hosting/configuration/schema-migrations.mdx b/docs/self-hosting/configuration/schema-migrations.mdx
index 6a94af751..5df52e713 100644
--- a/docs/self-hosting/configuration/schema-migrations.mdx
+++ b/docs/self-hosting/configuration/schema-migrations.mdx
@@ -1,11 +1,11 @@
---
title: "Schema migration"
-description: "Run Postgres schema migrations"
+description: "Learn how to run Postgres schema migrations."
---
Running schema migrations is a requirement before deploying Infisical.
Each time you decide to upgrade your version of Infisical, it's necessary to run schema migrations for that specific version.
-The guide below outlines a step-by-step guide to help you through this process.
+The guide below outlines a step-by-step guide to help you manually run schema migrations for Infisical.
### Prerequisites
- Docker installed on your machine
diff --git a/docs/self-hosting/deployment-options/aws-ec2.mdx b/docs/self-hosting/deployment-options/aws-ec2.mdx
deleted file mode 100644
index 303df2009..000000000
--- a/docs/self-hosting/deployment-options/aws-ec2.mdx
+++ /dev/null
@@ -1,21 +0,0 @@
----
-title: "AWS EC2"
-description: "Learn to install Infisical on EC2 using Cloud Formation template"
----
-
-VIDEO
-This deployment option will use AWS Cloudformation to auto deploy an instance of Infisical on a single EC2 via Docker Compose.
-
-**Resources that will be provisioned**
-- 1 EC2 instance
-- 1 DocumentDB cluster
-- 1 DocumentDB instance
-- Security groups
-
-
-Once installation is complete, you will have to create the first account. No default account is provided.
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/aws-lightsail.mdx b/docs/self-hosting/deployment-options/aws-lightsail.mdx
deleted file mode 100644
index b5cdb6c9d..000000000
--- a/docs/self-hosting/deployment-options/aws-lightsail.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
----
-title: "AWS Lightsail"
-description: "Deploy Infisical with AWS Lightsail"
----
-
-Prerequisites:
-- Have an account with [Amazon Web Services (AWS)](https://aws.amazon.com/)
-
-
-
- 1.1. In AWS, navigate to the **Lightsail** service and press **Create container service** under the **Containers** tab.
- 
-
- 
-
- 1.2. In the **Container service location** section, select the AWS region that's closest to your infrastructure.
-
- Afterwards, in the **Container service capacity** section, set the power level and scale to fit your needs; you may opt for the default setting
- and adjust accordingly in the future.
-
- 
-
- 1.3. In the **Set up your first deployment** section, select the **Specify a custom deployment** option. Give the container a friendly name like **infisical** and fill in your intended [Infisical public Docker image](https://hub.docker.com/r/infisical/infisical) in the **Image** field; this will pull the image from Docker Hub.
-
- For example, in order to opt for Infisical `v0.43.4`, you would input: `infisical/infisical:v0.43.4`.
-
- 
-
- 1.4. Running Infisical requires a few environment variables to be set for the container service.
- At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL`
- which you can read more about [here](/self-hosting/configuration/envars).
-
- In the **Environment variables** section, fill in the required environment variables.
-
-
- To use more features like emailing and single sign-on, you can set additional configuration options [here](/self-hosting/configuration/envars).
-
-
- Also, under the **Open ports** section, add an entry for port `8080` and protocol `HTTP` since Infisical listens on port `8080`.
-
- 
-
- 1.5. In the **Public endpoint** section, select the container from the previous steps from the dropdown; this will make the container accessible over the public internet.
-
- 
-
- 1.6. Finally, in the **Identify your service** section, give the container service a unique name like infisical and press **Create container service**.
-
- 
-
-
- On the newly-created container service page, wait for the **Status** to turn to **Running** and check out the **Public domain** of the container service; you can access your instance of Infisical by this URL.
-
- 
-
-
-
-
-
- Yes, here are a few that come to mind:
- - In step 1.3, we recommend pinning the Docker image to a specific [version of Infisical](https://hub.docker.com/r/infisical/infisical/tags)
- instead of referring to the `latest` tag to avoid any unexpected version-to-version migration issues.
-
- We're working on putting together a fuller list of deployment best practices as well as minimum resource configuration requirements for running Infisical so stay tuned!
-
-
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/azure-app-services.mdx b/docs/self-hosting/deployment-options/azure-app-services.mdx
deleted file mode 100644
index a8472ae2b..000000000
--- a/docs/self-hosting/deployment-options/azure-app-services.mdx
+++ /dev/null
@@ -1,71 +0,0 @@
----
-title: "Azure App Services"
-description: "Deploy Infisical with Azure App Service"
----
-
-Prerequisites:
- - Have an account with [Microsoft Azure](https://azure.microsoft.com/en-us)
-
-
-
- 1.1. In Azure, navigate to the **App Services** solution and press **Create > Web App**.
-
- 
-
- 
-
- 1.2. In the **Basics** section, specify the **Subscription** and **Resource group** to manage the deployed resource.
-
- Also, give the container a friendly name like Infisical and specify a **Region** for it to be deployed to.
-
- 
-
- 1.3. In the **Docker** section, select the **Single Container** option under **Options** and specify **Docker Hub** as the image source
-
- Next, under the **Docker hub options** sub-section, select the **Public** option under **Access Type** and fill in your intended [Infisical public Docker image](https://hub.docker.com/r/infisical/infisical) in the **Image and tag** field; this will pull the image from Docker Hub.
-
- For example, in order to opt for Infisical `v0.43.4`, you would input: `infisical/infisical:v0.43.4`.
-
- 
-
- 1.4. Finally, in the **Review + create** section, double check the information from the previous steps and press **Create** to create the Azure app service.
-
- 
-
- 1.5. Next, wait a minute or two on the deployment overview page for the app to be created. Once the deployment is complete, press **Go to resource**
- to head to the **App Service dashboard** for the newly-created app.
-
- 
-
- 1.6. Running Infisical requires a few environment variables to be set for the Azure app service.
- At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL`
- which you can read more about [here](/self-hosting/configuration/envars).
-
-
- To use more features like emailing and single sign-on, you can set additional configuration options [here](/self-hosting/configuration/envars).
-
-
- Additionally, you must set the variable `WEBSITES_PORT=8080` since
- Infisical listens on port `8080`.
-
- In the **Settings > Configuration** section of the newly-created app service, fill in the required environment variables.
-
- 
-
-
- In the **Overview** section, check out the **Default domain** for your instance of Infisical; you can visit the instance at this URL.
-
- 
-
-
-
-
-
- Yes, here are a few that come to mind:
- - In step 1.3, we recommend pinning the Docker image to a specific [version of Infisical](https://hub.docker.com/r/infisical/infisical/tags)
- instead of referring to the `latest` tag to avoid any unexpected version-to-version migration issues.
- - In step 1.2, we recommend selecting a **Region** option that is closest to your infrastructure/clients to reduce latency.
-
- We're working on putting together a fuller list of deployment best practices as well as minimum resource configuration requirements for running Infisical so stay tuned!
-
-
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/azure-container-instances.mdx b/docs/self-hosting/deployment-options/azure-container-instances.mdx
deleted file mode 100644
index 05e877f37..000000000
--- a/docs/self-hosting/deployment-options/azure-container-instances.mdx
+++ /dev/null
@@ -1,88 +0,0 @@
----
-title: "Azure Container Instances"
-description: "Deploy Infisical with Azure Container Instances"
----
-
-Prerequisites:
-- Have an account with [Microsoft Azure](https://azure.microsoft.com/en-us)
-
-
- This brief goes over how to deploy an instance of Infisical with Azure Container Instances without TLS/SSL configuration.
-
- There are various options for enabling TLS/SSL with Azure Container Instances more suitable for production including:
- - [Enabling a TLS endpoint in a sidecar container](https://learn.microsoft.com/en-us/azure/container-instances/container-instances-container-group-ssl).
- - [Enabling automatic HTTPS with Caddy in a sidecar container](https://learn.microsoft.com/en-us/azure/container-instances/container-instances-container-group-automatic-ssl).
- - Using Azure Function Proxies, Application Gateway, etc.
-
- For a simpler deployment experience with complete TLS/SSL setup, you may try [deploying Infisical with Azure App Services](/self-hosting/deployment-options/azure-app-services).
-
-
-
-
- 1.1. In Azure, navigate to the **Container Instances** solution and press **Create**.
-
- 
-
- 
-
- 1.2. In the **Basics** section, specify the **Subscription** and **Resource group** to manage the deployed resource.
-
- Also, give the container a friendly name like Infisical and specify a **Region** for it to be deployed to.
-
- 
-
- Next, select the **Public** option under **Image type** and fill in your intended [Infisical public Docker image](https://hub.docker.com/r/infisical/infisical) in the **Image** field; this will pull the image from Docker Hub.
-
- For example, in order to opt for Infisical `v0.43.4`, you would input: `infisical/infisical:v0.43.4`.
-
- 
-
-
- Depending on your use-case and requirements, you may find it helpful to further configure your Azure container instance.
-
- For example, you may want to adjust the **Region** option to specify which region to deploy the container for your
- instance of Infisical to minimize distance and therefore latency between the instance and your infrastructure.
-
-
- 1.3. In the **Networking** section, select the **Public** option under **Networking type**; this will make the container accessible over the public internet.
-
- Next, under the **Ports** section, add an entry for port `8080` and protocol `TCP` since Infisical listens on port `8080`.
-
- 
-
- 1.4. Running Infisical requires a few environment variables to be set for the Azure container instance.
- At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL`
- which you can read more about [here](/self-hosting/configuration/envars).
-
- In the **Advanced** section, fill in the required environment variables.
-
-
- To use more features like emailing and single sign-on, you can set additional configuration options [here](/self-hosting/configuration/envars).
-
-
- 
-
- 1.5. Finally, in the **Review + create** section, double check the information from the previous steps and press **Create** to create the Azure container instance.
-
- 
-
-
- Head to the **Overview** page of the newly-created container instance to view its **IP address (Public)**; you can access your instance of Infisical by this IP address under the port `:8080`.
-
- For example, in the image below, the IP address of the sample deployed container instance is `4.255.87.109`; the instance would be accessible in the browser by heading to `4.255.87.109:8080`.
-
- 
-
-
-
-
-
- Yes, here are a few that come to mind:
- - In step 1.2, we recommend pinning the Docker image to a specific [version of Infisical](https://hub.docker.com/r/infisical/infisical/tags)
- instead of referring to the `latest` tag to avoid any unexpected version-to-version migration issues.
- - In step 1.2, we recommend selecting a **Region** option that is closest to your infrastructure/clients to reduce latency.
- - Enable TLS/SSL with Azure Container Instances. There are various options for doing so including [enabling a TLS endpoint in a sidecar container](https://learn.microsoft.com/en-us/azure/container-instances/container-instances-container-group-ssl), [enabling automatic HTTPS with Caddy in a sidecar container](https://learn.microsoft.com/en-us/azure/container-instances/container-instances-container-group-automatic-ssl), and using Azure Function Proxies, Application Gateway, etc.
-
- We're working on putting together a fuller list of deployment best practices as well as minimum resource configuration requirements for running Infisical so stay tuned!
-
-
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/digital-ocean-marketplace.mdx b/docs/self-hosting/deployment-options/digital-ocean-marketplace.mdx
deleted file mode 100644
index f1e739f08..000000000
--- a/docs/self-hosting/deployment-options/digital-ocean-marketplace.mdx
+++ /dev/null
@@ -1,27 +0,0 @@
----
-title: "Digital Ocean"
-description: "Learn to install Infisical on Digital Ocean"
----
-
-Infisical can be deployed on a Kubernetes cluster with a single click through our Digital Ocean marketplace application.
-The initiation of the installation process triggers the creation of a Kubernetes cluster, followed by the installation of Infisical onto that cluster.
-
-This automated deployment method uses the same process under the hood as the manual [Kubernetes installation guide](./kubernetes-helm).
-
-### Initiate the installation
-
-To start the process, click the following button and follow the instructions there.
-
-
-
-
-
-### Access Infisical Web
-Once the installation finishes, head to the `Networking` section via the sidebar and select `Load Balancers`.
-Within this section, you'll find the newly created load balancer for Infisical. You can access Infisical at the IP address allocated to that load balancer.
-
-### Adjusting configurations
-If you need to either upgrade or downgrade Infisical, or modify environment variables to alter its functionality, refer to our [Kubernetes installation](./kubernetes-helm) page for detailed instructions.
-
-Because Digital Ocean deploys the same Helm application as described in our [Kubernetes installation](./kubernetes-helm) guide, you can utilize that guide to implement the required changes.
-It's important to note that any modifications requires familiarly with Helm package manager.
diff --git a/docs/self-hosting/deployment-options/docker-compose.mdx b/docs/self-hosting/deployment-options/docker-compose.mdx
index 583fe1674..291a91370 100644
--- a/docs/self-hosting/deployment-options/docker-compose.mdx
+++ b/docs/self-hosting/deployment-options/docker-compose.mdx
@@ -1,6 +1,6 @@
---
title: "Docker Compose"
-description: "Run Infisical with Docker Compose template"
+description: "Read how to run Infisical with Docker Compose template."
---
Install Infisical using Docker compose. This self hosting method contains all of the required components needed
to run a functional instance of Infisical.
@@ -80,4 +80,4 @@ docker-compose -f docker-compose.prod.yml up
Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`.
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/fly.io.mdx b/docs/self-hosting/deployment-options/fly.io.mdx
deleted file mode 100644
index dacd9476b..000000000
--- a/docs/self-hosting/deployment-options/fly.io.mdx
+++ /dev/null
@@ -1,108 +0,0 @@
----
-title: "Fly.io"
-description: "Deploy Infisical with Fly.io"
----
-
-Prerequisites:
-- Have an account with [Fly.io](https://fly.io/)
-- Have installed the [Fly.io CLI](https://fly.io/docs/hands-on/install-flyctl/)
-
-
-
- In your terminal, run the following command from the source directory of your project to create a new Fly.io app
- with a `fly.toml` configuration file:
-
- ```
- fly launch
- ```
-
-
- Add a **build** section to the `fly.toml` file to specify the [Infisical public Docker image](https://hub.docker.com/r/infisical/infisical):
-
- ```
- [build]
- image = "infisical/infisical:v0.43.4"
- ```
-
- Afterwards, your `fly.toml` file should look similar to:
-
- ```
- app = "infisical"
- primary_region = "lax"
-
- [http_service]
- internal_port = 8080
- force_https = true
- auto_stop_machines = true
- auto_start_machines = true
- min_machines_running = 0
- processes = ["app"]
-
- [[vm]]
- cpu_kind = "shared"
- cpus = 1
- memory_mb = 1024
-
- [build]
- image = "infisical/infisical:v0.43.4"
- ```
-
-
- Depending on your use-case and requirements, you may find it helpful to further configure your `fly.toml` file
- with options [here](https://fly.io/docs/reference/configuration/).
-
- For example, you may want to adjust the `primary-region` option to specify which [region](https://fly.io/docs/reference/regions/) to create the new machine for your
- instance of Infisical to minimize distance and therefore latency between the instance and your infrastructure.
-
-
-
-
- Running Infisical requires a few environment variables to be set on the Fly.io machine.
- At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL`
- which you can read more about [here](/self-hosting/configuration/envars).
-
- For this step, we recommend setting the variables as Fly.io [app secrets](https://fly.io/docs/reference/secrets/) which
- are made available to the app as environment variables. You can set the variables either via the Fly.io CLI or project [dashboard](https://fly.io/dashboard).
-
-
-
- Run the following command (with each `VALUE` replaced) in the source directory of your project to set the required variables:
-
- ```
- flyctl secrets set ENCRYPTION_KEY=VALUE AUTH_SECRET=VALUE MONGO_URL=VALUE REDIS_URL=VALUE...
- ```
-
-
- In Fly.io, head to your Project > Secrets and add the required variables.
-
- 
-
-
-
-
- To use more features like emailing and single sign-on, you can set additional configuration options [here](/self-hosting/configuration/envars).
-
-
-
- Finally, run the following command in the source directory of your project to deploy your Infisical instance on Fly.io
- with the updated `fly.toml` configuration file from step 2 and secrets from step 3:
-
- ```
- fly deploy
- ```
-
-
-
-
-
- Yes, here are a few that come to mind:
- - In step 2, we recommend pinning the Docker image to a specific [version of Infisical](https://hub.docker.com/r/infisical/infisical/tags)
- instead of referring to the `latest` tag to avoid any unexpected version-to-version migration issues.
- - In step 2, we recommend selecting a `primary_region` option that is closest to your infrastructure/clients to reduce latency; a full list of regions supported by Fly.io can be found [here](https://fly.io/docs/reference/regions/).
-
- We're working on putting together a fuller list of deployment best practices as well as minimum resource configuration requirements for running Infisical so stay tuned!
-
-
-
-Resources:
-- [Fly.io documentation](https://fly.io/docs/)
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/gcp-cloud-run.mdx b/docs/self-hosting/deployment-options/gcp-cloud-run.mdx
deleted file mode 100644
index 67c9fcf57..000000000
--- a/docs/self-hosting/deployment-options/gcp-cloud-run.mdx
+++ /dev/null
@@ -1,67 +0,0 @@
----
-title: "GCP Cloud Run"
-description: "Deploy Infisical with GCP Cloud Run"
----
-
-Prerequisites:
-- Have an account with [Google Cloud Platform (GCP)](https://cloud.google.com/)
-
-
-
- In GCP, create a new project and give it a friendly name like Infisical.
-
- 
-
- 
-
-
- 2.1. Inside the GCP project, navigate to the **Cloud Run** product and create a new service.
-
- 
-
- 
-
- 2.2. In the service creation form, select the **Deploy one revision from an existing container image** option and fill in your intended [Infisical public Docker image](https://hub.docker.com/r/infisical/infisical) in the container image URL.
-
- For example, in order to opt for Infisical `v0.43.4`, you would input: `docker.io/infisical/infisical:v0.43.4`.
-
- 
-
- 2.3. Running Infisical requires a few environment variables to be set for the GCP Cloud Run service.
- At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL`
- which you can read more about [here](/self-hosting/configuration/envars).
-
- For this step, fill in the required environment variables in the Edit Container > Variables & Secrets > Environment variables section.
-
-
- To use more features like emailing and single sign-on, you can set additional configuration options [here](/self-hosting/configuration/envars).
-
-
- 
-
-
- Depending on your use-case and requirements, you may find it helpful to further configure your GCP Cloud Run service.
-
- For example, you may want to adjust the **Region** option to specify which region to deploy the underlying container for your
- instance of Infisical to minimize distance and therefore latency between the instance and your infrastructure.
-
-
- Finally, press **Create** to finish setting up the GCP Cloud Run service.
-
-
- Head to the **Service details** of the newly-created service to view its URL; you can access your instance of Infisical by clicking on the URL.
-
- 
-
-
-
-
-
- Yes, here are a few that come to mind:
- - In step 2, we recommend pinning the Docker image to a specific [version of Infisical](https://hub.docker.com/r/infisical/infisical/tags)
- instead of referring to the `latest` tag to avoid any unexpected version-to-version migration issues.
- - In step 2, we recommend selecting a **Region** option that is closest to your infrastructure/clients to reduce latency.
-
- We're working on putting together a fuller list of deployment best practices as well as minimum resource configuration requirements for running Infisical so stay tuned!
-
-
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx
index ae27207cd..b95a3fd4d 100644
--- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx
+++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx
@@ -1,6 +1,6 @@
---
title: "Kubernetes via Helm Chart"
-description: "Use Helm chart to install Infisical on your Kubernetes cluster"
+description: "Learn how to use Helm chart to install Infisical on your Kubernetes cluster."
---
**Prerequisites**
- You have extensive understanding of [Kubernetes](https://kubernetes.io/)
@@ -173,7 +173,7 @@ description: "Use Helm chart to install Infisical on your Kubernetes cluster"
After deployment, please wait for 2-5 minutes for all pods to reach a running state. Once a significant number of pods are operational, access the IP address revealed through Ingress by your load balancer.
You can find the IP address/hostname by executing the command `kubectl get ingress`.
- 
+ 
To upgrade your instance of Infisical simply update the docker image tag in your Halm values and rerun the command below.
diff --git a/docs/self-hosting/deployment-options/railway.mdx b/docs/self-hosting/deployment-options/railway.mdx
deleted file mode 100644
index 29d2ce293..000000000
--- a/docs/self-hosting/deployment-options/railway.mdx
+++ /dev/null
@@ -1,61 +0,0 @@
----
-title: "Railway"
-description: "Deploy Infisical with Railway"
----
-
-Prerequisites:
-- Have an account with [Railway](https://railway.app/)
-
-
-
- 1.1. In Railway, create a new project and select **Deploy a template > Infisical**.
-
- 
-
- 
-
- 
-
- 
-
- 1.2. At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL`
- which you can read more about [here](/self-hosting/configuration/envars).
-
- By default, the Infisical template on Railway pre-configures environment variables on each service in the deployment but requires you to supply two for the Redis and MongoDB services.
-
- On the MongoDB service, supply a value for the `MONGO_INITDB_ROOT_PASSWORD` variable.
-
- 
-
- On the Redis service, supply a value for the `REDIS_PASSWORD` variable.
-
- 
-
- 
-
-
- To use more features like emailing and single sign-on, you can set additional configuration options on the Infisical service [here](/self-hosting/configuration/envars).
-
-
- Finally, press **Deploy** to create the project and deploy the services within it.
-
- 
-
- 
-
-
- Head to the newly-created Infisical service to view its URL under Networking > Public Networking; you can access your instance of Infisical by clicking on the URL.
-
- 
-
-
-
-
-
- Yes, here are a few that come to mind:
- - While the Infisical template on Railway uses the `latest` tag to get the latest version of Infisical, we recommend creating a Railway deployment that pins the Docker image to a specific [version of Infisical](https://hub.docker.com/r/infisical/infisical/tags) to avoid any unexpected version-to-version migration issues.
- - We recommend selecting **Deployment region** options for your Railway service deployments to be closest to your infrastructure/clients to reduce latency.
-
- We're working on putting together a fuller list of deployment best practices as well as minimum resource configuration requirements for running Infisical so stay tuned!
-
-
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/render.mdx b/docs/self-hosting/deployment-options/render.mdx
deleted file mode 100644
index 17faf060a..000000000
--- a/docs/self-hosting/deployment-options/render.mdx
+++ /dev/null
@@ -1,21 +0,0 @@
----
-title: "Render.com"
-description: "Learn to install Infisical Render.com"
----
-
-**Prerequisites**
-- An account at Render.com
-- A document DB instance
-
-Deploying on Render is one of the quickest ways to have Infisical running in production.
-Before you start deployment, you will need to obtain document db connection string. This will be used for `MONGO_URL` environment variable required during installation.
-
-You can create a document db database using services such as [MongoDB](https://www.mongodb.com/), [AWS DocumentDB](https://aws.amazon.com/documentdb/), and others. Once done, click the link below to start deployment.
-
-### **[Deploy to Render](https://render.com/deploy?repo=https://github.com/Infisical/infisical)**
-
-#
-
-
-Once installation is complete, you will have to create the first account. No default account is provided.
-
\ No newline at end of file
diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx
index 740792578..ab1512612 100644
--- a/docs/self-hosting/deployment-options/standalone-infisical.mdx
+++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx
@@ -1,6 +1,6 @@
---
title: "Docker"
-description: "Run Infisical with Docker"
+description: "Learn how to run Infisical with Docker."
---
Prerequisites:
@@ -52,7 +52,7 @@ The following guide provides a detailed step-by-step walkthrough on how you can
Once the container is running, verify the installation by opening your web browser and navigating to `http://localhost:80`.
- 
+ 
diff --git a/docs/self-hosting/ee.mdx b/docs/self-hosting/ee.mdx
new file mode 100644
index 000000000..a72bad908
--- /dev/null
+++ b/docs/self-hosting/ee.mdx
@@ -0,0 +1,29 @@
+---
+title: "Infisical Enterprise"
+description: "Find out how to activate Infisical Enterprise edition (EE) features."
+---
+
+While most features in Infisical are free to use, others are paid and require purchasing an enterprise license to use them.
+
+This guide walks through how you can use these paid features on a self hosted instance of Infisical.
+
+
+
+ Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting sales@infisical.com to purchase a license.
+
+ Once purchased, you will be issued a license key.
+
+
+ Depending on whether or not the environment where Infisical is deployed has internet access, you may be issued a regular license or an offline license.
+
+ - If using a regular license, you should set the value of the environment variable `LICENSE_KEY` in Infisical to the issued license key.
+ - If using an offline license, you should set the value of the environment variable `LICENSE_KEY_OFFLINE` in Infisical to the issued license key.
+
+
+ How you set the environment variable will depend on the deployment method you used. Please refer to the documentation of your deployment method for specific instructions.
+
+
+ Once your instance starts up, the license key will be validated and youโll be able to use the paid features.
+ However, when the license expires, Infisical will continue to run, but EE features will be disabled until the license is renewed or a new one is purchased.
+
+
diff --git a/docs/self-hosting/faq.mdx b/docs/self-hosting/faq.mdx
index 598d408ae..db98a23dc 100644
--- a/docs/self-hosting/faq.mdx
+++ b/docs/self-hosting/faq.mdx
@@ -1,10 +1,10 @@
---
title: "FAQ"
-description: "Frequently Asked Questions about Infisical self hosting"
+description: "Frequently Asked Questions about self-hosting Infisical."
---
Frequently asked questions about self hosted instance of Infisical can be found on this page.
-If you can't find the answer you are looking for, please create an issue on our GitHub repository or join our Slack channel for additional support.
+If you can't find the answer you are looking for, please create an issue on our [GitHub repository](https://github.com/Infisical/infisical) or join our [Slack community](https://infisical.com/slack) for additional support.
This issue is typically seen when you haven't set up SSL for your self hosted instance of Infisical. When SSL is not enabled, you can't receive secure cookies, preventing the session data to not be saved.
diff --git a/docs/self-hosting/guides/mongo-to-postgres.mdx b/docs/self-hosting/guides/mongo-to-postgres.mdx
index f8a6cca0f..b8781a19d 100644
--- a/docs/self-hosting/guides/mongo-to-postgres.mdx
+++ b/docs/self-hosting/guides/mongo-to-postgres.mdx
@@ -1,6 +1,6 @@
---
title: "Migrate Mongo to Postgres"
-description: "How to migrate from MongoDB to PostgreSQL for Infisical"
+description: "Learn how to migrate Infisical from MongoDB to PostgreSQL."
---
This guide will provide step by step instructions on migrating your Infisical instance running on MongoDB to the newly released PostgreSQL version of Infisical.
diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx
index f8089719d..ccc4ae912 100644
--- a/docs/self-hosting/overview.mdx
+++ b/docs/self-hosting/overview.mdx
@@ -1,31 +1,35 @@
---
-title: "Introduction"
-description: "Self-host Infisical on your own infrastructure"
+title: ""
+sidebarTitle: "Introduction"
+description: "Learn how to self-host Infisical on your own infrastructure."
---
Self-hosting Infisical lets you retain data on your own infrastructure and network.
-Choose from a variety of deployment options listed below to get started.
+Choose from a number of deployment options listed below to get started.
- Use the fully packaged docker image to deploy Infisical anywhere
+ Use the fully packaged docker image to deploy Infisical anywhere.
- Install Infisical using our Docker Compose template
+ Install Infisical using our Docker Compose template.
- Use our Helm chart to Install Infisical on your Kubernetes cluster
+ Use our Helm chart to Install Infisical on your Kubernetes cluster.
diff --git a/docs/self-hosting/reference-architectures/aws-ecs.mdx b/docs/self-hosting/reference-architectures/aws-ecs.mdx
new file mode 100644
index 000000000..a4ce4a2b6
--- /dev/null
+++ b/docs/self-hosting/reference-architectures/aws-ecs.mdx
@@ -0,0 +1,56 @@
+---
+title: "AWS ECS"
+description: "Reference architecture for self-hosting Infisical on AWS ECS"
+---
+
+This guide will provide high-level architecture design for deploying the Infisical on AWS ECS and give insights into the core components, high availability strategies, and secure credential management for Infisical's root secrets.
+
+## Overview
+
+In this guide, we'll focus on running Infisical on AWS Elastic Container Service (ECS) across multiple Availability Zones (AZs), ensuring high availability and resilience.
+The architecture utilizes Amazon Relational Database Service (RDS) for persistent storage, ElastiCache for Redis as an in-memory data store for caching, and Amazon Simple Email Service (SES) to handle email based communications from Infisical.
+
+
+
+
+### Core Components
+
+- **ECS Fargate:** In this architecture, Infisical is deployed on ECS using Fargate launch type. The ECS services are deployed across multiple Availability Zones to ensure high availability.
+
+- **Amazon RDS:** Infisical uses Postgres as it's persistent layer. As such, RDS for PostgreSQL is used as the database engine. The setup includes a primary instance in one AZ and a read replica in another AZ.
+This ensures that if there is a failure in one availability zone, the working replica will become the primary and continue processing workloads.
+
+- **Amazon ElastiCache for Redis:** To enhance performance, Infisical requires Redis. In this architecture, Redis is set up with a primary and standby replication group across two AZs to increase availability.
+
+- **Amazon Simple Email Service (SES):** Infisical requires email service to facilitate outbound communication. AWS SES is integrated into the architecture to handle such communication.
+
+### Network Setup
+
+- **Public Subnets:** Each Availability Zone contains a public subnet. There are two main reasons you might need internet access. First, if you intend to use Infisical to communicate with external secrets managers not located within your virtual private network, enabling internet access is necessary. Second, downloading the Docker image from Docker Hub requires internet access, though this can be avoided by utilizing AWS ECR with VPC Endpoints through AWS Private Link.
+
+- **NAT Gateway:** This is used to route outbound requests from Infisical to the internet and is only used to communicate with external secrets manager and or downloading container images.
+
+### Securing Infisical's root credential
+
+- **Parameter Store:** To secure Infisical's root credentials (database connection string, encryption key, etc), we highly recommend that you use AWS Parameter Store and only allow the tasks running Infisical to access them.
+- **AWS Secrets Manager:** We strongly advise securing the master credentials for RDS by utilizing the latest AWS RDS integration with AWS Secrets Manager. This integration automatically stores the master database user's credentials in AWS Secrets Manager, thereby reducing the risk of misplacing the root RDS credential.
+
+### High Availability (HA) and Scalability
+
+- **Multi-AZ Deployment:** By spreading resources across multiple Availability Zones, we ensure that if one AZ experiences issues, traffic can be redirected to the remaining healthy AZ without service interruption.
+
+- **Auto Scaling:** AWS Auto Scaling is in place to adjust capacity to maintain steady and predictable performance at the lowest possible cost.
+
+- **Cross-Region Deployment:** For even greater high availability, you may deploy Infisical across multiple regions. This extends the HA capabilities of the architecture and protects against regional service disruptions.
+
+
+### Frequently asked questions
+
+ Yes, Infisical can function in an air-gapped environment. To do so, update your ECS task to use the publicly available AWS Elastic Container Registry (ECR) image instead of the default Docker Hub image. Additionally, it's necessary to configure VPC endpoints, which allows your system to access AWS ECR via a private network route instead of the internet, ensuring all connectivity remains within the secure, private network.
+
+
+ Since the Amazon RDS instance is housed within a private network to enhance security, it is not directly accessible from the internet. This means that in order to run the required [Postgres schema migrations](/self-hosting/configuration/schema-migrations), you need to connect to this instance of RDS. There are many approaches you can take:
+ - To automate schema migrations, you may setup CI/CD pipeline with access to the same RDS network to run the schema migrations before making deployment to ECS. This ensures that if migrations fail, your Infisical instances continues to run.
+ - If you would like to run the migrations manually, consider using AWS Systems Manager Session Manager to access the RDS within the VPC on your local machine.
+ - If your organization already has mechanisms in place for secure access to the VPC, such as VPNs or Direct Connect, these can also be utilized for performing database migrations manually.
+
diff --git a/docs/self-hosting/reference-architectures/on-premise.mdx b/docs/self-hosting/reference-architectures/on-premise.mdx
new file mode 100644
index 000000000..543e9e938
--- /dev/null
+++ b/docs/self-hosting/reference-architectures/on-premise.mdx
@@ -0,0 +1,70 @@
+---
+title: "On-premise"
+description: "Reference architecture for self-hosting Infisical on premise"
+---
+
+Deploying Infisical on-premise with high availability requires deep knowledge in areas like networking, container orchestration, and database management.
+This guide presents a reference architecture that outlines how to achieve such a deployment effectively.
+For organizations that do not have the necessary resources or expertise, we recommend opting for managed, dedicated Infisical instances or engaging professional services to mitigate the complexities.
+
+## System Overview
+
+
+The architecture above utilizes a combination of Kubernetes for orchestrating stateless components and virtual machines (VMs) or bare metal for stateful components.
+The infrastructure spans multiple data centers for redundancy and load distribution, enhancing availability and disaster recovery capabilities.
+You may duplicate the architecture in multiple data centers and join them via Consul to increase availability. This way, if one data center is out of order, active data centers will take over workloads.
+
+### Stateful vs stateless workloads
+
+To reduce the challenges of managing state within Kubernetes, including storage provisioning, persistent volume management, and intricate data backup and recovery processes, we strongly recommend deploying stateful components on Virtual Machines (VMs) or bare metal.
+As depicted in the architecture, Infisical is intentionally deployed on Kubernetes to leverage its strengths in managing stateless applications.
+Being stateless, Infisical fully benefits from Kubernetes' features like horizontal scaling, self-healing, and rolling updates and rollbacks.
+
+## Core Components
+
+### Kubernetes Cluster
+Infisical is deployed on a Kubernetes cluster, which allows for container management, auto-scaling, and self-healing capabilities.
+A load balancer sits in front of the Kubernetes cluster, directing traffic and ensuring even load distribution across the application nodes.
+This is the entry point where all other services will interact with Infisical.
+
+
+### Consul as the Networking Backbone
+Consul is an critical component in the reference architecture, serving as a unified service networking layer that links and controls services across different environments and data centers.
+It functions as the common communication channel between data centers for stateless applications on Kubernetes and stateful services such as databases on dedicated VMs or bare metal.
+
+
+### Postgres with Patroni
+The database layer is powered by Postgres, with [Patroni](https://patroni.readthedocs.io/en/latest/) providing automated management to create a high availability setup. Patroni leverages Consul for several critical operations:
+
+- **Redundancy:** By managing a cluster of one primary and multiple secondary Postgres nodes, the architecture ensures redundancy.
+The primary node handles all the write operations, and secondary nodes handle read operations and are prepared to step up in case of primary failure.
+
+- **Failover and Service Discovery:** Consul is integrated with Patroni for service discovery and health checks.
+When Patroni detects that the primary node is unhealthy, it uses Consul to elect a new primary node from the secondaries, thereby ensuring that the database service remains available.
+
+- **Data Center Awareness:** Patroni configured with Consul is aware of the multi-data center setup and can handle failover across data centers if necessary, which further enhances the system's availability.
+
+### Redis with Redis Sentinel
+For caching and message brokering:
+
+- Redis is deployed with a primary-replica setup.
+- Redis Sentinel monitors the Redis nodes, providing automatic failover and service discovery.
+- Write operations go to the primary node, and replicas serve read operations, ensuring data integrity and availability.
+
+## Multi data center deployment
+Infisical can be deployed across a number of data centers to both increase performance and resiliency to disaster scenarios.
+For mission critical deployment of Infisical, we recommend deploying Infisical on at least 3 data centers to reduce downtime in the event of complete data center malfunction.
+
+### Data Center A
+Data Center A houses the primary nodes of both Postgres and Redis, which handle all write operations. The secondary nodes and replicas serve as hot standbys for failover. Consul servers maintain the state of the cluster, elect a leader, and facilitate service discovery.
+
+### $n^{th}$ data center
+The $n^{th}$ data center acts as a performance and disaster recovery site, featuring a mesh gateway that enables cross-data center service discovery and configuration. It houses additional secondary nodes for Postgres and Redis replicas, which are ready to be promoted in case the primary data center fails. Additionally, this data center can reduce the latency of applications that need to interact with Infisical, particularly if those applications or services are geographically closer to this data center.
+
+## Considerations
+
+The complexity of an on-premise deployment scales with the level of availability required. This reference architecture provides a robust framework for organizations aiming for high availability and disaster resilience. However, it's important to recognize that this is not a one-size-fits-all solution.
+
+Organizations with less stringent Recovery Time Objectives (RTO) might find that [simpler deployments methods](/self-hosting/deployment-options/docker-compose) using tools such as Docker Compose are adequate. Such setups can still provide a reasonable level of service continuity without the complexities involved in managing a multi-data center environment with Kubernetes, Consul, and other high-availability components.
+
+Ultimately, the choice of architecture should be guided by a thorough analysis of business needs, available resources, and expertise.
\ No newline at end of file
diff --git a/docs/style.css b/docs/style.css
new file mode 100644
index 000000000..b76d06450
--- /dev/null
+++ b/docs/style.css
@@ -0,0 +1,142 @@
+#navbar .max-w-8xl {
+ max-width: 100%;
+ border-bottom: 1px solid #ebebeb;
+ background-color: #fcfcfc;
+}
+
+.max-w-8xl {
+ /* background-color: #f5f5f5; */
+}
+
+#sidebar {
+ left: 0;
+ padding-left: 48px;
+ padding-right: 30px;
+ border-right: 1px;
+ border-color: #cdd64b;
+ background-color: #fcfcfc;
+ border-right: 1px solid #ebebeb;
+}
+
+#sidebar .relative .sticky {
+ opacity: 0;
+}
+
+#sidebar li > div.mt-2 {
+ border-radius: 0;
+ padding: 5px;
+}
+
+#sidebar li > a.mt-2 {
+ border-radius: 0;
+ padding: 5px;
+}
+
+#sidebar li > a.leading-6 {
+ border-radius: 0;
+ padding: 0px;
+}
+
+/* #sidebar ul > div.mt-12 {
+ padding-top: 30px;
+ position: relative;
+}
+
+#sidebar ul > div.mt-12 h5 {
+ position: absolute;
+ left: -12px;
+ top: -0px;
+} */
+
+#header {
+ border-left: 1px solid #26272b;
+ padding-left: 16px;
+ padding-right: 16px;
+ background-color: #f5f5f5;
+ padding-bottom: 10px;
+ padding-top: 10px;
+}
+
+#content-area .mt-8 .block{
+ border-radius: 0;
+ border-width: 1px;
+ border-color: #ebebeb;
+}
+
+#content-area .mt-8 .rounded-xl{
+ border-radius: 0;
+}
+
+#content-area .mt-8 .rounded-lg{
+ border-radius: 0;
+}
+
+#content-area .mt-6 .rounded-xl{
+ border-radius: 0;
+}
+
+#content-area .mt-6 .rounded-lg{
+ border-radius: 0;
+}
+
+#content-area .mt-6 .rounded-md{
+ border-radius: 0;
+}
+
+#content-area .mt-8 .rounded-md{
+ border-radius: 0;
+}
+
+#content-area div.my-4{
+ border-radius: 0;
+ border-width: 1px;
+}
+
+#content-area div.flex-1 {
+ /* text-transform: uppercase; */
+ opacity: 0.8;
+ font-weight: 400;
+}
+
+#content-area button {
+ border-radius: 0;
+}
+
+#content-area a {
+ border-radius: 0;
+}
+
+#content-area .not-prose {
+ border-radius: 0;
+}
+
+/* .eyebrow {
+ text-transform: uppercase;
+ font-weight: 400;
+ color: red;
+} */
+
+#content-container {
+ /* background-color: #f5f5f5; */
+ margin-top: 2rem;
+}
+
+#topbar-cta-button .group .absolute {
+ background-color: black;
+ border-radius: 0px;
+}
+
+/* #topbar-cta-button .group .absolute:hover {
+ background-color: white;
+ border-radius: 0px;
+} */
+
+#topbar-cta-button .group .flex {
+ margin-top: 5px;
+ margin-bottom: 5px;
+ font-size: medium;
+}
+
+.flex-1 .flex .items-center {
+ /* background-color: #f5f5f5; */
+}
\ No newline at end of file
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
index 6666aaabf..13e8e5ab7 100644
--- a/frontend/.eslintrc.js
+++ b/frontend/.eslintrc.js
@@ -29,6 +29,7 @@ module.exports = {
},
plugins: ["react", "prettier", "simple-import-sort", "import"],
rules: {
+ "@typescript-eslint/no-empty-function": "off",
quotes: ["error", "double", { avoidEscape: true }],
"comma-dangle": ["error", "only-multiline"],
"react/react-in-jsx-scope": "off",
@@ -72,7 +73,6 @@ module.exports = {
],
"@typescript-eslint/no-non-null-assertion": "off",
"simple-import-sort/exports": "warn",
- "@typescript-eslint/no-empty-function": "off",
"simple-import-sort/imports": [
"warn",
{
diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js
index 83c1ca9e3..1a68699d2 100644
--- a/frontend/.storybook/main.js
+++ b/frontend/.storybook/main.js
@@ -1,28 +1,28 @@
-const path = require('path');
+const path = require("path");
module.exports = {
- stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+ stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
- '@storybook/addon-links',
- '@storybook/addon-essentials',
- '@storybook/addon-interactions',
- 'storybook-dark-mode',
+ "@storybook/addon-links",
+ "@storybook/addon-essentials",
+ "@storybook/addon-interactions",
+ "storybook-dark-mode",
{
- name: '@storybook/addon-styling',
+ name: "@storybook/addon-styling",
options: {
postCss: {
- implementation: require('postcss')
+ implementation: require("postcss")
}
}
}
],
framework: {
- name: '@storybook/nextjs',
+ name: "@storybook/nextjs",
options: {}
},
core: {
disableTelemetry: true
},
docs: {
- autodocs: 'tag'
+ autodocs: "tag"
}
};
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 2060c214a..9090fc603 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -52,6 +52,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG 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 NEXT_INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4b7907b60..e7c587f13 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -67,6 +67,7 @@
"jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0",
"markdown-it": "^13.0.1",
+ "ms": "^2.1.3",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
@@ -84,6 +85,7 @@
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
+ "react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
"set-cookie-parser": "^2.5.1",
"sharp": "^0.33.2",
@@ -9963,13 +9965,13 @@
"dev": true
},
"node_modules/body-parser": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
- "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+ "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
- "content-type": "~1.0.4",
+ "content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
@@ -9977,7 +9979,7 @@
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
- "raw-body": "2.5.1",
+ "raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
@@ -10926,9 +10928,9 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"dev": true,
"engines": {
"node": ">= 0.6"
@@ -11507,6 +11509,11 @@
}
}
},
+ "node_modules/debug/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
"node_modules/decode-named-character-reference": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
@@ -13307,17 +13314,17 @@
}
},
"node_modules/express": {
- "version": "4.18.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
- "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
+ "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
- "body-parser": "1.20.1",
+ "body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.5.0",
+ "cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -13811,9 +13818,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -15178,9 +15185,9 @@
}
},
"node_modules/ip": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
- "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
+ "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
"dev": true
},
"node_modules/ipaddr.js": {
@@ -17570,9 +17577,9 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/multipipe": {
"version": "1.0.2",
@@ -19838,9 +19845,9 @@
}
},
"node_modules/raw-body": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
- "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
@@ -20345,6 +20352,26 @@
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
}
},
+ "node_modules/react-toastify": {
+ "version": "9.1.3",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz",
+ "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==",
+ "dependencies": {
+ "clsx": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/react-toastify/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -21339,12 +21366,6 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
- },
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -23752,9 +23773,9 @@
}
},
"node_modules/webpack-dev-middleware": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz",
- "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==",
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz",
+ "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==",
"dev": true,
"dependencies": {
"colorette": "^2.0.10",
diff --git a/frontend/package.json b/frontend/package.json
index cae75f0c8..e01ef945e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -75,6 +75,7 @@
"jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0",
"markdown-it": "^13.0.1",
+ "ms": "^2.1.3",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
@@ -92,6 +93,7 @@
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
+ "react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
"set-cookie-parser": "^2.5.1",
"sharp": "^0.33.2",
diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh
index d9138bb77..859814eda 100755
--- a/frontend/scripts/initialize-standalone-build.sh
+++ b/frontend/scripts/initialize-standalone-build.sh
@@ -4,6 +4,8 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
+scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
+
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"
scripts/set-standalone-build-telemetry.sh true
diff --git a/frontend/scripts/start.sh b/frontend/scripts/start.sh
index 1db867e15..1488ad328 100644
--- a/frontend/scripts/start.sh
+++ b/frontend/scripts/start.sh
@@ -4,6 +4,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_P
scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
+scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
+
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"
scripts/set-telemetry.sh true
diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts
index 246d5c9cc..cc26e5512 100644
--- a/frontend/src/components/analytics/posthog.ts
+++ b/frontend/src/components/analytics/posthog.ts
@@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config";
export const initPostHog = () => {
// @ts-ignore
- console.log("Hi there ๐")
+ console.log("Hi there ๐");
try {
if (typeof window !== "undefined") {
// @ts-ignore
@@ -19,7 +19,7 @@ export const initPostHog = () => {
return posthog;
} catch (e) {
- console.log("posthog err", e)
+ console.log("posthog err", e);
}
return undefined;
diff --git a/frontend/src/components/basic/Error.tsx b/frontend/src/components/basic/Error.tsx
index bf892e03d..1ef937e4a 100644
--- a/frontend/src/components/basic/Error.tsx
+++ b/frontend/src/components/basic/Error.tsx
@@ -3,9 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const Error = ({ text }: { text: string }): JSX.Element => {
return (
-
-
- {text &&
{text}
}
+
);
};
diff --git a/frontend/src/components/basic/InputField.tsx b/frontend/src/components/basic/InputField.tsx
index 0bc01defc..346294e56 100644
--- a/frontend/src/components/basic/InputField.tsx
+++ b/frontend/src/components/basic/InputField.tsx
@@ -39,16 +39,16 @@ const InputField = ({
if (isStatic === true) {
return (
-
-
{label}
- {text &&
{text}
}
+
+
{label}
+ {text &&
{text}
}
onChangeHandler(e.target.value)}
type={type}
placeholder={placeholder}
value={value}
required={isRequired}
- className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none"
+ className="text-md min-w-16 w-full rounded-md border border-gray-600 bg-bunker-800 p-2 text-gray-400 outline-none"
name={name}
readOnly
autoComplete={autoComplete}
@@ -58,12 +58,12 @@ const InputField = ({
);
}
return (
-
-
-
{label}
+
+
@@ -75,11 +75,11 @@ const InputField = ({
required={isRequired}
className={`${
blurred
- ? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400"
+ ? "text-bunker-800 focus:text-gray-400 active:text-gray-400 group-hover:text-gray-400"
: ""
} ${
error ? "focus:ring-red/50" : "focus:ring-primary/50"
- } relative peer bg-mineshaft-900 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
+ } text-md min-w-16 peer relative w-full rounded-md bg-mineshaft-900 p-2 text-gray-400 outline-none duration-200 focus:ring-4`}
name={name}
spellCheck="false"
autoComplete={autoComplete}
@@ -91,7 +91,7 @@ const InputField = ({
onClick={() => {
setPasswordVisible(!passwordVisible);
}}
- className="absolute self-end mr-3 text-gray-400 cursor-pointer"
+ className="absolute mr-3 cursor-pointer self-end text-gray-400"
>
{passwordVisible ? (
@@ -101,7 +101,7 @@ const InputField = ({
)}
{blurred && (
-
+
{value
.split("")
@@ -109,7 +109,7 @@ const InputField = ({
.map(() => (
))}
@@ -121,7 +121,7 @@ const InputField = ({
)} */}
- {error &&
{errorText}
}
+ {error &&
{errorText}
}
);
};
diff --git a/frontend/src/components/basic/Listbox.tsx b/frontend/src/components/basic/Listbox.tsx
index 5cdeb26d9..cad9aaab4 100644
--- a/frontend/src/components/basic/Listbox.tsx
+++ b/frontend/src/components/basic/Listbox.tsx
@@ -34,19 +34,19 @@ const ListBox = ({
{text}
-
+
{" "}
{isSelected}
{data && (
-
+
)}
@@ -58,16 +58,16 @@ const ListBox = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
-
+
{data.map((person, personIdx) => (
- `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
- selected ? "bg-white/10 text-gray-400 font-bold" : ""
+ `relative my-0.5 cursor-default select-none rounded-md py-2 pl-10 pr-4 ${
+ selected ? "bg-white/10 font-bold text-gray-400" : ""
} ${
active && !selected
- ? "bg-white/5 text-mineshaft-200 cursor-pointer"
+ ? "cursor-pointer bg-white/5 text-mineshaft-200"
: "text-gray-400"
} `
}
@@ -83,7 +83,7 @@ const ListBox = ({
{person}
{selected ? (
-
+
) : null}
@@ -92,9 +92,9 @@ const ListBox = ({
))}
{buttonAction && (
-
-
-
+
+