Compare commits
179 Commits
fix/db-hos
...
dynamic-1
Author | SHA1 | Date | |
---|---|---|---|
|
0fdf5032f9 | ||
|
e258b84796 | ||
|
1ab6b21b25 | ||
|
775037539e | ||
|
7c623562e1 | ||
|
aef8d79101 | ||
|
d735ec71b8 | ||
|
84651d473b | ||
|
9501386882 | ||
|
d11f958443 | ||
|
087a4bb7d2 | ||
|
750210e6c3 | ||
|
90cf4e9137 | ||
|
17bb2e8a7d | ||
|
b912cd585c | ||
|
282434de8e | ||
|
1f939a5e58 | ||
|
ac0f5369de | ||
|
6eba64c975 | ||
|
12515c1866 | ||
|
c882da2e1a | ||
|
8a7774f9ac | ||
|
a7d2ec80c6 | ||
|
494543ec53 | ||
|
b7b875b6a7 | ||
|
3ddd06a3d1 | ||
|
a1a8364cd1 | ||
|
3e51fcb546 | ||
|
c52a16cc47 | ||
|
f91c77baa3 | ||
|
e7c2f6f88c | ||
|
f7c2d38aef | ||
|
cfb497dd58 | ||
|
f7122c21fd | ||
|
b23deca8e4 | ||
|
b606990dfb | ||
|
2240277243 | ||
|
c8c5caba62 | ||
|
f408a6f60c | ||
|
391ed0ed74 | ||
|
aef40212d2 | ||
|
5aa7cd46c1 | ||
|
6c0b916ad8 | ||
|
d7bc80308d | ||
|
b7c7b242e8 | ||
|
b592f4cb6d | ||
|
cd0e1a87cf | ||
|
b5d7699b8d | ||
|
69297bc16e | ||
|
37827367ed | ||
|
403b1ce993 | ||
|
c3c0006a25 | ||
|
2241908d0a | ||
|
59b822510c | ||
|
d1408aff35 | ||
|
c67084f08d | ||
|
a280e002ed | ||
|
76c4a8660f | ||
|
8c54dd611e | ||
|
98ea2c1828 | ||
|
5c75f526e7 | ||
|
113e777b25 | ||
|
2a93449ffe | ||
|
1ef1c042da | ||
|
b64672a921 | ||
|
227e013502 | ||
|
88f7e4255e | ||
|
44ca8c315e | ||
|
7766a7f4dd | ||
|
3cb150a749 | ||
|
9e9ce261c8 | ||
|
fab7167850 | ||
|
c7de9aab4e | ||
|
3560346f85 | ||
|
f0bf2f8dd0 | ||
|
2a6216b8fc | ||
|
a07d055347 | ||
|
c05230f667 | ||
|
d68055a264 | ||
|
e3e62430ba | ||
|
dc6056b564 | ||
|
94f0811661 | ||
|
7b84ae6173 | ||
|
5710a304f8 | ||
|
91e3bbba34 | ||
|
02112ede07 | ||
|
08cfbf64e4 | ||
|
18da522b45 | ||
|
8cf68fbd9c | ||
|
d6b82dfaa4 | ||
|
7bd4eed328 | ||
|
0341c32da0 | ||
|
caea055281 | ||
|
c08c78de8d | ||
|
3765a14246 | ||
|
c5a11e839b | ||
|
93bd3d8270 | ||
|
b9601dd418 | ||
|
ae3bc04b07 | ||
|
11edefa66f | ||
|
f71459ede0 | ||
|
33324a5a3c | ||
|
5c6781a705 | ||
|
71e31518d7 | ||
|
f6f6db2898 | ||
|
55780b65d3 | ||
|
83bbf9599d | ||
|
f8f2b2574d | ||
|
318d12addd | ||
|
872a28d02a | ||
|
6f53a5631c | ||
|
ff2098408d | ||
|
9e85d9bbf0 | ||
|
0f3a48bb32 | ||
|
f869def8ea | ||
|
378bc57a88 | ||
|
242179598b | ||
|
70fe80414d | ||
|
e201e80a06 | ||
|
177cd385cc | ||
|
ab48c3b4fe | ||
|
69f36d1df6 | ||
|
11c7b5c674 | ||
|
ee29577e6d | ||
|
e3e049b66c | ||
|
878e4a79e7 | ||
|
609ce8e5cc | ||
|
04c1ea9b11 | ||
|
3baca73e53 | ||
|
36adf6863b | ||
|
6363e7d30a | ||
|
f9621fad8e | ||
|
90be28b87a | ||
|
671adee4d7 | ||
|
c9cb90c98e | ||
|
9f691df395 | ||
|
d702a61586 | ||
|
1c16f406a7 | ||
|
90f739caa6 | ||
|
ede8b6f286 | ||
|
232c547d75 | ||
|
fe08bbb691 | ||
|
2bd06ecde4 | ||
|
08b79d65ea | ||
|
4e1733ba6c | ||
|
a4e495ea1c | ||
|
a750d68363 | ||
|
d7161a353d | ||
|
12c414817f | ||
|
e5e494d0ee | ||
|
5a21b85e9e | ||
|
348fdf6429 | ||
|
88e609cb66 | ||
|
78058d691a | ||
|
1d465a50c3 | ||
|
ffc7249c7c | ||
|
90bcf23097 | ||
|
5fa4d9029d | ||
|
7160cf58ee | ||
|
6b2d757e39 | ||
|
c075fcceca | ||
|
e25f5dd65f | ||
|
3eef023c30 | ||
|
e63deb0860 | ||
|
02b2851990 | ||
|
d2a93eb1d2 | ||
|
fa1b28b33f | ||
|
415cf31b2d | ||
|
9002e6cb33 | ||
|
1ede551c3e | ||
|
b7b43858f6 | ||
|
203e00216f | ||
|
ee215bccfa | ||
|
7a3a6663f1 | ||
|
8c491668dc | ||
|
c873e2cba8 | ||
|
1bc045a7fa | ||
|
533de93199 | ||
|
115b4664bf |
@@ -3,9 +3,6 @@
|
|||||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
|
|
||||||
# Required
|
|
||||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Required secrets to sign JWT tokens
|
# Required secrets to sign JWT tokens
|
||||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# 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_USER=infisical
|
||||||
POSTGRES_DB=infisical
|
POSTGRES_DB=infisical
|
||||||
|
|
||||||
|
# Required
|
||||||
|
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
|
@@ -41,6 +41,7 @@ jobs:
|
|||||||
load: true
|
load: true
|
||||||
context: backend
|
context: backend
|
||||||
tags: infisical/infisical:test
|
tags: infisical/infisical:test
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: ⏻ Spawn backend container and dependencies
|
- name: ⏻ Spawn backend container and dependencies
|
||||||
run: |
|
run: |
|
||||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||||
@@ -92,6 +93,7 @@ jobs:
|
|||||||
project: 64mmf0n610
|
project: 64mmf0n610
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:test
|
tags: infisical/frontend:test
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
build-args: |
|
build-args: |
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
|
140
.github/workflows/build-staging-and-deploy-aws.yml
vendored
Normal file
@@ -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
|
120
.github/workflows/build-staging-and-deploy.yml
vendored
@@ -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
|
|
@@ -118,9 +118,6 @@ WORKDIR /backend
|
|||||||
|
|
||||||
ENV TELEMETRY_ENABLED true
|
ENV TELEMETRY_ENABLED true
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
|
||||||
CMD node healthcheck.js
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
||||||
|
@@ -10,7 +10,8 @@
|
|||||||
<a href="https://infisical.com/">Infisical Cloud</a> |
|
<a href="https://infisical.com/">Infisical Cloud</a> |
|
||||||
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
|
||||||
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
|
||||||
<a href="https://www.infisical.com">Website</a>
|
<a href="https://www.infisical.com">Website</a> |
|
||||||
|
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
6
backend/src/@types/fastify.d.ts
vendored
@@ -21,6 +21,8 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
|||||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { TDynamicSecretServiceFactory } from "@app/services/dynamic-secret/dynamic-secret-service";
|
||||||
|
import { TDynamicSecretLeaseServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
@@ -62,7 +64,7 @@ declare module "fastify" {
|
|||||||
authMethod: ActorAuthMethod;
|
authMethod: ActorAuthMethod;
|
||||||
type: ActorType;
|
type: ActorType;
|
||||||
id: string;
|
id: string;
|
||||||
orgId?: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
// passport data
|
// passport data
|
||||||
passportUser: {
|
passportUser: {
|
||||||
@@ -117,6 +119,8 @@ declare module "fastify" {
|
|||||||
trustedIp: TTrustedIpServiceFactory;
|
trustedIp: TTrustedIpServiceFactory;
|
||||||
secretBlindIndex: TSecretBlindIndexServiceFactory;
|
secretBlindIndex: TSecretBlindIndexServiceFactory;
|
||||||
telemetry: TTelemetryServiceFactory;
|
telemetry: TTelemetryServiceFactory;
|
||||||
|
dynamicSecret: TDynamicSecretServiceFactory;
|
||||||
|
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
12
backend/src/@types/knex.d.ts
vendored
@@ -17,6 +17,12 @@ import {
|
|||||||
TBackupPrivateKey,
|
TBackupPrivateKey,
|
||||||
TBackupPrivateKeyInsert,
|
TBackupPrivateKeyInsert,
|
||||||
TBackupPrivateKeyUpdate,
|
TBackupPrivateKeyUpdate,
|
||||||
|
TDynamicSecretLeases,
|
||||||
|
TDynamicSecretLeasesInsert,
|
||||||
|
TDynamicSecretLeasesUpdate,
|
||||||
|
TDynamicSecrets,
|
||||||
|
TDynamicSecretsInsert,
|
||||||
|
TDynamicSecretsUpdate,
|
||||||
TGitAppInstallSessions,
|
TGitAppInstallSessions,
|
||||||
TGitAppInstallSessionsInsert,
|
TGitAppInstallSessionsInsert,
|
||||||
TGitAppInstallSessionsUpdate,
|
TGitAppInstallSessionsUpdate,
|
||||||
@@ -340,6 +346,12 @@ declare module "knex/types/tables" {
|
|||||||
TSecretSnapshotFoldersInsert,
|
TSecretSnapshotFoldersInsert,
|
||||||
TSecretSnapshotFoldersUpdate
|
TSecretSnapshotFoldersUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.DynamicSecret]: Knex.CompositeTableType<TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate>;
|
||||||
|
[TableName.DynamicSecretLease]: Knex.CompositeTableType<
|
||||||
|
TDynamicSecretLeases,
|
||||||
|
TDynamicSecretLeasesInsert,
|
||||||
|
TDynamicSecretLeasesUpdate
|
||||||
|
>;
|
||||||
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
|
||||||
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
|
||||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||||
|
58
backend/src/db/migrations/20240318164718_dynamic-secret.ts
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.DynamicSecretLease);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.DynamicSecretLease);
|
||||||
|
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.DynamicSecret);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.DynamicSecret);
|
||||||
|
}
|
24
backend/src/db/schemas/dynamic-secret-leases.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const 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<typeof DynamicSecretLeasesSchema>;
|
||||||
|
export type TDynamicSecretLeasesInsert = Omit<z.input<typeof DynamicSecretLeasesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TDynamicSecretLeasesUpdate = Partial<Omit<z.input<typeof DynamicSecretLeasesSchema>, TImmutableDBKeys>>;
|
31
backend/src/db/schemas/dynamic-secrets.ts
Normal file
@@ -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<typeof DynamicSecretsSchema>;
|
||||||
|
export type TDynamicSecretsInsert = Omit<z.input<typeof DynamicSecretsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TDynamicSecretsUpdate = Partial<Omit<z.input<typeof DynamicSecretsSchema>, TImmutableDBKeys>>;
|
@@ -3,6 +3,8 @@ export * from "./audit-logs";
|
|||||||
export * from "./auth-token-sessions";
|
export * from "./auth-token-sessions";
|
||||||
export * from "./auth-tokens";
|
export * from "./auth-tokens";
|
||||||
export * from "./backup-private-key";
|
export * from "./backup-private-key";
|
||||||
|
export * from "./dynamic-secret-leases";
|
||||||
|
export * from "./dynamic-secrets";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
export * from "./git-app-org";
|
export * from "./git-app-org";
|
||||||
export * from "./identities";
|
export * from "./identities";
|
||||||
|
@@ -59,6 +59,8 @@ export enum TableName {
|
|||||||
GitAppOrg = "git_app_org",
|
GitAppOrg = "git_app_org",
|
||||||
SecretScanningGitRisk = "secret_scanning_git_risks",
|
SecretScanningGitRisk = "secret_scanning_git_risks",
|
||||||
TrustedIps = "trusted_ips",
|
TrustedIps = "trusted_ips",
|
||||||
|
DynamicSecret = "dynamic_secrets",
|
||||||
|
DynamicSecretLease = "dynamic_secret_leases",
|
||||||
// junction tables with tags
|
// junction tables with tags
|
||||||
JnSecretTag = "secret_tag_junction",
|
JnSecretTag = "secret_tag_junction",
|
||||||
SecretVersionTag = "secret_version_tag_junction"
|
SecretVersionTag = "secret_version_tag_junction"
|
||||||
|
@@ -19,7 +19,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(1)
|
.min(1)
|
||||||
.trim()
|
.trim()
|
||||||
.refine(
|
.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"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
)
|
)
|
||||||
.refine((v) => slugify(v) === v, {
|
.refine((v) => slugify(v) === v, {
|
||||||
|
@@ -146,7 +146,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
offset: req.query.startIndex,
|
offset: req.query.startIndex,
|
||||||
limit: req.query.count,
|
limit: req.query.count,
|
||||||
filter: req.query.filter,
|
filter: req.query.filter,
|
||||||
orgId: req.permission.orgId as string
|
orgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await req.server.services.scim.getScimUser({
|
const user = await req.server.services.scim.getScimUser({
|
||||||
userId: req.params.userId,
|
userId: req.params.userId,
|
||||||
orgId: req.permission.orgId as string
|
orgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
email: primaryEmail,
|
email: primaryEmail,
|
||||||
firstName: req.body.name.givenName,
|
firstName: req.body.name.givenName,
|
||||||
lastName: req.body.name.familyName,
|
lastName: req.body.name.familyName,
|
||||||
orgId: req.permission.orgId as string
|
orgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@@ -280,7 +280,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await req.server.services.scim.updateScimUser({
|
const user = await req.server.services.scim.updateScimUser({
|
||||||
userId: req.params.userId,
|
userId: req.params.userId,
|
||||||
orgId: req.permission.orgId as string,
|
orgId: req.permission.orgId,
|
||||||
operations: req.body.Operations
|
operations: req.body.Operations
|
||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
@@ -330,7 +330,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await req.server.services.scim.replaceScimUser({
|
const user = await req.server.services.scim.replaceScimUser({
|
||||||
userId: req.params.userId,
|
userId: req.params.userId,
|
||||||
orgId: req.permission.orgId as string,
|
orgId: req.permission.orgId,
|
||||||
active: req.body.active
|
active: req.body.active
|
||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
|
@@ -15,6 +15,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
membersUsed: 0,
|
membersUsed: 0,
|
||||||
environmentLimit: null,
|
environmentLimit: null,
|
||||||
environmentsUsed: 0,
|
environmentsUsed: 0,
|
||||||
|
dynamicSecret: true,
|
||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: false,
|
pitRecovery: false,
|
||||||
ipAllowlisting: false,
|
ipAllowlisting: false,
|
||||||
|
@@ -27,6 +27,7 @@ export type TFeatureSet = {
|
|||||||
tier: -1;
|
tier: -1;
|
||||||
workspaceLimit: null;
|
workspaceLimit: null;
|
||||||
workspacesUsed: 0;
|
workspacesUsed: 0;
|
||||||
|
dynamicSecret: true;
|
||||||
memberLimit: null;
|
memberLimit: null;
|
||||||
membersUsed: 0;
|
membersUsed: 0;
|
||||||
environmentLimit: null;
|
environmentLimit: null;
|
||||||
|
@@ -194,6 +194,25 @@ export const FOLDERS = {
|
|||||||
}
|
}
|
||||||
} as const;
|
} 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 = {
|
export const RAW_SECRETS = {
|
||||||
LIST: {
|
LIST: {
|
||||||
workspaceId: "The ID of the project to list secrets from.",
|
workspaceId: "The ID of the project to list secrets from.",
|
||||||
@@ -285,3 +304,96 @@ export const AUDIT_LOGS = {
|
|||||||
actor: "The actor to filter the audit logs by."
|
actor: "The actor to filter the audit logs by."
|
||||||
}
|
}
|
||||||
} as const;
|
} 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;
|
||||||
|
@@ -18,6 +18,7 @@ const envSchema = z
|
|||||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
|
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}`
|
`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_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
|
||||||
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
|
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
|
||||||
DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"),
|
DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"),
|
||||||
|
@@ -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 {
|
export class ScimRequestError extends Error {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ export type TProjectPermission = {
|
|||||||
actorId: string;
|
actorId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string | undefined;
|
actorOrgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequiredKeys<T> = {
|
export type RequiredKeys<T> = {
|
||||||
|
@@ -18,7 +18,8 @@ export enum QueueName {
|
|||||||
SecretWebhook = "secret-webhook",
|
SecretWebhook = "secret-webhook",
|
||||||
SecretFullRepoScan = "secret-full-repo-scan",
|
SecretFullRepoScan = "secret-full-repo-scan",
|
||||||
SecretPushEventScan = "secret-push-event-scan",
|
SecretPushEventScan = "secret-push-event-scan",
|
||||||
UpgradeProjectToGhost = "upgrade-project-to-ghost"
|
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
||||||
|
DynamicSecretRevocation = "dynamic-secret-revocation"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -30,7 +31,9 @@ export enum QueueJobs {
|
|||||||
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
||||||
IntegrationSync = "secret-integration-pull",
|
IntegrationSync = "secret-integration-pull",
|
||||||
SecretScan = "secret-scan",
|
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 = {
|
export type TQueueJobTypes = {
|
||||||
@@ -86,6 +89,19 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.TelemetryInstanceStats;
|
name: QueueJobs.TelemetryInstanceStats;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
};
|
};
|
||||||
|
[QueueName.DynamicSecretRevocation]:
|
||||||
|
| {
|
||||||
|
name: QueueJobs.DynamicSecretRevocation;
|
||||||
|
payload: {
|
||||||
|
leaseId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.DynamicSecretPruning;
|
||||||
|
payload: {
|
||||||
|
dynamicSecretCfgId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
@@ -16,7 +16,7 @@ export type TAuthMode =
|
|||||||
userId: string;
|
userId: string;
|
||||||
tokenVersionId: string; // the session id of token used
|
tokenVersionId: string; // the session id of token used
|
||||||
user: TUsers;
|
user: TUsers;
|
||||||
orgId?: string;
|
orgId: string;
|
||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -119,7 +119,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
tokenVersionId,
|
tokenVersionId,
|
||||||
actor,
|
actor,
|
||||||
orgId,
|
orgId: orgId as string,
|
||||||
authMethod: token.authMethod
|
authMethod: token.authMethod
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
@@ -47,6 +47,12 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
|
|||||||
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||||
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { dynamicSecretDALFactory } from "@app/services/dynamic-secret/dynamic-secret-dal";
|
||||||
|
import { dynamicSecretServiceFactory } from "@app/services/dynamic-secret/dynamic-secret-service";
|
||||||
|
import { buildDynamicSecretProviders } from "@app/services/dynamic-secret/providers";
|
||||||
|
import { dynamicSecretLeaseDALFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
|
import { dynamicSecretLeaseQueueServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||||
|
import { dynamicSecretLeaseServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||||
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
@@ -196,6 +202,8 @@ export const registerRoutes = async (
|
|||||||
const gitAppOrgDAL = gitAppDALFactory(db);
|
const gitAppOrgDAL = gitAppDALFactory(db);
|
||||||
const secretScanningDAL = secretScanningDALFactory(db);
|
const secretScanningDAL = secretScanningDALFactory(db);
|
||||||
const licenseDAL = licenseDALFactory(db);
|
const licenseDAL = licenseDALFactory(db);
|
||||||
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
|
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
@@ -550,6 +558,34 @@ export const registerRoutes = async (
|
|||||||
licenseService
|
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();
|
await superAdminService.initServerCfg();
|
||||||
//
|
//
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@@ -591,6 +627,8 @@ export const registerRoutes = async (
|
|||||||
secretApprovalPolicy: sapService,
|
secretApprovalPolicy: sapService,
|
||||||
secretApprovalRequest: sarService,
|
secretApprovalRequest: sarService,
|
||||||
secretRotation: secretRotationService,
|
secretRotation: secretRotationService,
|
||||||
|
dynamicSecret: dynamicSecretService,
|
||||||
|
dynamicSecretLease: dynamicSecretLeaseService,
|
||||||
snapshot: snapshotService,
|
snapshot: snapshotService,
|
||||||
saml: samlService,
|
saml: samlService,
|
||||||
ldap: ldapService,
|
ldap: ldapService,
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
import { z } from "zod";
|
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
|
// sometimes the return data must be santizied to avoid leaking important values
|
||||||
// always prefer pick over omit in zod
|
// always prefer pick over omit in zod
|
||||||
@@ -56,3 +61,11 @@ export const secretRawSchema = z.object({
|
|||||||
secretValue: z.string(),
|
secretValue: z.string(),
|
||||||
secretComment: z.string().optional()
|
secretComment: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||||
|
inputIV: true,
|
||||||
|
inputTag: true,
|
||||||
|
inputCiphertext: true,
|
||||||
|
keyEncoding: true,
|
||||||
|
algorithm: true
|
||||||
|
});
|
||||||
|
185
backend/src/server/routes/v1/dynamic-secret-lease-router.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { SanitizedDynamicSecretSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
|
export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
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({
|
||||||
|
url: "/:leaseId",
|
||||||
|
method: "DELETE",
|
||||||
|
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({
|
||||||
|
url: "/:leaseId/renew",
|
||||||
|
method: "POST",
|
||||||
|
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",
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
272
backend/src/server/routes/v1/dynamic-secret-router.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
|
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||||
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { DynamicSecretProviderSchema } from "@app/services/dynamic-secret/providers/models";
|
||||||
|
|
||||||
|
import { SanitizedDynamicSecretSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
|
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
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({
|
||||||
|
url: "/:name",
|
||||||
|
method: "PATCH",
|
||||||
|
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({
|
||||||
|
url: "/:name",
|
||||||
|
method: "DELETE",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,6 +1,8 @@
|
|||||||
import { registerAdminRouter } from "./admin-router";
|
import { registerAdminRouter } from "./admin-router";
|
||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
import { registerIdentityRouter } from "./identity-router";
|
import { registerIdentityRouter } from "./identity-router";
|
||||||
import { registerIdentityUaRouter } from "./identity-ua";
|
import { registerIdentityUaRouter } from "./identity-ua";
|
||||||
@@ -52,6 +54,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (dynamicSecretRouter) => {
|
||||||
|
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||||
|
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
|
||||||
|
},
|
||||||
|
{ prefix: "/dynamic-secrets" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
await server.register(registerProjectBotRouter, { prefix: "/bot" });
|
||||||
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretTagsSchema } from "@app/db/schemas";
|
import { SecretTagsSchema } from "@app/db/schemas";
|
||||||
|
import { SECRET_TAGS } from "@app/lib/api-docs";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim()
|
projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -36,12 +37,12 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim()
|
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
|
||||||
slug: z.string().trim(),
|
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
|
||||||
color: z.string()
|
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -68,8 +69,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim(),
|
projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
|
||||||
tagId: z.string().trim()
|
tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
import { RAW_SECRETS } from "@app/lib/api-docs";
|
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
@@ -23,6 +23,124 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
|||||||
import { secretRawSchema } from "../sanitizedSchemas";
|
import { secretRawSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/tags/:secretName",
|
||||||
|
method: "POST",
|
||||||
|
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({
|
||||||
|
url: "/tags/:secretName",
|
||||||
|
method: "DELETE",
|
||||||
|
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({
|
server.route({
|
||||||
url: "/raw",
|
url: "/raw",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@@ -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<typeof dynamicSecretLeaseDALFactory>;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
@@ -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<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||||
|
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||||
|
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,341 @@
|
|||||||
|
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 { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||||
|
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
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<TDynamicSecretDALFactory, "findOne">;
|
||||||
|
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||||
|
dynamicSecretQueueService: TDynamicSecretLeaseQueueServiceFactory;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
@@ -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<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDetailsDynamicSecretLeaseDTO = {
|
||||||
|
leaseId: string;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListDynamicSecretLeasesDTO = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteDynamicSecretLeaseDTO = {
|
||||||
|
leaseId: string;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
isForced?: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TRenewDynamicSecretLeaseDTO = {
|
||||||
|
leaseId: string;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
ttl?: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
10
backend/src/services/dynamic-secret/dynamic-secret-dal.ts
Normal file
@@ -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<typeof dynamicSecretDALFactory>;
|
||||||
|
|
||||||
|
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.DynamicSecret);
|
||||||
|
return orm;
|
||||||
|
};
|
341
backend/src/services/dynamic-secret/dynamic-secret-service.ts
Normal file
@@ -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 { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
|
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
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<TDynamicSecretLeaseDALFactory, "find">;
|
||||||
|
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||||
|
dynamicSecretQueueService: Pick<
|
||||||
|
TDynamicSecretLeaseQueueServiceFactory,
|
||||||
|
"pruneDynamicSecret" | "unsetLeaseRevocation"
|
||||||
|
>;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
54
backend/src/services/dynamic-secret/dynamic-secret-types.ts
Normal file
@@ -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<typeof DynamicSecretProviderSchema>;
|
||||||
|
export type TCreateDynamicSecretDTO = {
|
||||||
|
provider: TProvider;
|
||||||
|
defaultTTL: string;
|
||||||
|
maxTTL?: string | null;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
name: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateDynamicSecretDTO = {
|
||||||
|
name: string;
|
||||||
|
newName?: string;
|
||||||
|
defaultTTL?: string;
|
||||||
|
maxTTL?: string | null;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
inputs?: TProvider["inputs"];
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteDynamicSecretDTO = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
isForced?: boolean;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDetailsDynamicSecretDTO = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListDynamicSecretsDTO = {
|
||||||
|
path: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
6
backend/src/services/dynamic-secret/providers/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { DynamicSecretProviders } from "./models";
|
||||||
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
|
|
||||||
|
export const buildDynamicSecretProviders = () => ({
|
||||||
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
|
||||||
|
});
|
34
backend/src/services/dynamic-secret/providers/models.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export enum SqlProviders {
|
||||||
|
Postgres = "postgres"
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
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<boolean>;
|
||||||
|
validateProviderInputs: (inputs: object) => Promise<unknown>;
|
||||||
|
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
|
||||||
|
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
|
||||||
|
};
|
113
backend/src/services/dynamic-secret/providers/sql-database.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
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, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
|
const generatePassword = (size?: number) => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 48)(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
|
||||||
|
throw new BadRequestError({ message: "Invalid db host" });
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||||
|
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,
|
||||||
|
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
|
ssl,
|
||||||
|
pool: { min: 0, max: 1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const db = await getClient(providerInputs);
|
||||||
|
const isConnected = await db
|
||||||
|
.raw("SELECT NOW()")
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
await db.destroy();
|
||||||
|
return isConnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const db = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = alphaNumericNanoId(32);
|
||||||
|
const password = generatePassword();
|
||||||
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
expiration
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.raw(creationStatement.toString());
|
||||||
|
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 revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
|
await db.raw(revokeStatement);
|
||||||
|
|
||||||
|
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 renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
||||||
|
await db.raw(renewStatement);
|
||||||
|
|
||||||
|
await db.destroy();
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@@ -328,7 +328,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
);
|
);
|
||||||
const hasCustomRole = Boolean(customInputRoles.length);
|
const hasCustomRole = Boolean(customInputRoles.length);
|
||||||
if (hasCustomRole) {
|
if (hasCustomRole) {
|
||||||
const plan = await licenseService.getPlan(actorOrgId as string);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan?.rbac)
|
if (!plan?.rbac)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||||
|
@@ -168,8 +168,12 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findProjectBySlug = async (slug: string, orgId: string) => {
|
const findProjectBySlug = async (slug: string, orgId: string | undefined) => {
|
||||||
try {
|
try {
|
||||||
|
if (!orgId) {
|
||||||
|
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
|
||||||
|
}
|
||||||
|
|
||||||
const projects = await db(TableName.ProjectMembership)
|
const projects = await db(TableName.ProjectMembership)
|
||||||
.where(`${TableName.Project}.slug`, slug)
|
.where(`${TableName.Project}.slug`, slug)
|
||||||
.where(`${TableName.Project}.orgId`, orgId)
|
.where(`${TableName.Project}.orgId`, orgId)
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -284,10 +284,11 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
// Get the role permission for the identity
|
// Get the role permission for the identity
|
||||||
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
ProjectMembershipRole.Admin,
|
OrgMembershipRole.Member,
|
||||||
organization.id
|
organization.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Identity has to be at least a member in order to create projects
|
||||||
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
if (!hasPrivilege)
|
if (!hasPrivilege)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
|
@@ -232,6 +232,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
if (!parentFolder) return [];
|
if (!parentFolder) return [];
|
||||||
|
|
||||||
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
|
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
|
||||||
|
|
||||||
return folders;
|
return folders;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -150,6 +150,27 @@ 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 findByBlindIndexes = async (
|
const findByBlindIndexes = async (
|
||||||
folderId: string,
|
folderId: string,
|
||||||
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
|
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
|
||||||
@@ -184,6 +205,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
deleteMany,
|
deleteMany,
|
||||||
bulkUpdateNoVersionIncrement,
|
bulkUpdateNoVersionIncrement,
|
||||||
|
getSecretTags,
|
||||||
findByFolderId,
|
findByFolderId,
|
||||||
findByBlindIndexes
|
findByBlindIndexes
|
||||||
};
|
};
|
||||||
|
@@ -22,6 +22,7 @@ import { TSecretDALFactory } from "./secret-dal";
|
|||||||
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
|
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
|
||||||
import { TSecretQueueFactory } from "./secret-queue";
|
import { TSecretQueueFactory } from "./secret-queue";
|
||||||
import {
|
import {
|
||||||
|
TAttachSecretTagsDTO,
|
||||||
TCreateBulkSecretDTO,
|
TCreateBulkSecretDTO,
|
||||||
TCreateSecretDTO,
|
TCreateSecretDTO,
|
||||||
TCreateSecretRawDTO,
|
TCreateSecretRawDTO,
|
||||||
@@ -47,7 +48,7 @@ type TSecretServiceFactoryDep = {
|
|||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
secretVersionDAL: TSecretVersionDALFactory;
|
secretVersionDAL: TSecretVersionDALFactory;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
|
||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
@@ -307,6 +308,7 @@ export const secretServiceFactory = ({
|
|||||||
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
||||||
|
|
||||||
const { secretName, ...el } = inputSecret;
|
const { secretName, ...el } = inputSecret;
|
||||||
|
|
||||||
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
||||||
fnSecretBulkUpdate({
|
fnSecretBulkUpdate({
|
||||||
folderId,
|
folderId,
|
||||||
@@ -442,6 +444,7 @@ export const secretServiceFactory = ({
|
|||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
|
|
||||||
const secrets = await secretDAL.findByFolderId(folderId, actorId);
|
const secrets = await secretDAL.findByFolderId(folderId, actorId);
|
||||||
|
|
||||||
if (includeImports) {
|
if (includeImports) {
|
||||||
const secretImports = await secretImportDAL.find({ folderId });
|
const secretImports = await secretImportDAL.find({ folderId });
|
||||||
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
||||||
@@ -994,7 +997,209 @@ export const secretServiceFactory = ({
|
|||||||
return secretVersions;
|
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 {
|
return {
|
||||||
|
attachTags,
|
||||||
|
detachTags,
|
||||||
createSecret,
|
createSecret,
|
||||||
deleteSecret,
|
deleteSecret,
|
||||||
updateSecret,
|
updateSecret,
|
||||||
|
@@ -206,6 +206,15 @@ export type TFnSecretBulkUpdate = {
|
|||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAttachSecretTagsDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
secretName: string;
|
||||||
|
tagSlugs: string[];
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
type: SecretType;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TFnSecretBulkDelete = {
|
export type TFnSecretBulkDelete = {
|
||||||
folderId: string;
|
folderId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
1
cli/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.infisical.json
|
.infisical.json
|
||||||
dist/
|
dist/
|
||||||
|
agent-config.test.yaml
|
||||||
|
@@ -406,14 +406,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
|
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
|
||||||
var secretsResponse GetEncryptedSecretsV3Response
|
var secretsResponse GetEncryptedSecretsV3Response
|
||||||
response, err := httpClient.
|
response, err := httpClient.
|
||||||
R().
|
R().
|
||||||
SetResult(&secretsResponse).
|
SetResult(&secretsResponse).
|
||||||
SetHeader("User-Agent", USER_AGENT).
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
SetBody(request).
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
|
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
|
||||||
@@ -535,3 +535,23 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
|
|||||||
|
|
||||||
return getRawSecretsV3Response, nil
|
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
|
||||||
|
}
|
||||||
|
@@ -401,7 +401,6 @@ type DeleteSecretV3Request struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateSecretByNameV3Request struct {
|
type UpdateSecretByNameV3Request struct {
|
||||||
SecretName string `json:"secretName"`
|
|
||||||
WorkspaceID string `json:"workspaceId"`
|
WorkspaceID string `json:"workspaceId"`
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -501,6 +500,28 @@ type UniversalAuthRefreshResponse struct {
|
|||||||
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
|
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 {
|
type GetRawSecretsV3Request struct {
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
WorkspaceId string `json:"workspaceId"`
|
WorkspaceId string `json:"workspaceId"`
|
||||||
|
@@ -14,6 +14,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -33,6 +34,9 @@ import (
|
|||||||
|
|
||||||
const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"
|
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 {
|
type Config struct {
|
||||||
Infisical InfisicalConfig `yaml:"infisical"`
|
Infisical InfisicalConfig `yaml:"infisical"`
|
||||||
Auth AuthConfig `yaml:"auth"`
|
Auth AuthConfig `yaml:"auth"`
|
||||||
@@ -84,6 +88,115 @@ type Template struct {
|
|||||||
} `yaml:"config"`
|
} `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) {
|
func ReadFile(filePath string) ([]byte, error) {
|
||||||
return ioutil.ReadFile(filePath)
|
return ioutil.ReadFile(filePath)
|
||||||
}
|
}
|
||||||
@@ -234,15 +347,43 @@ 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
|
// custom template function to fetch secrets from Infisical
|
||||||
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag)
|
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag)
|
||||||
|
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateId)
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"secret": secretFunction,
|
"secret": secretFunction,
|
||||||
|
"dynamic_secret": dynamicSecretFunction,
|
||||||
}
|
}
|
||||||
|
|
||||||
templateName := path.Base(templatePath)
|
templateName := path.Base(templatePath)
|
||||||
|
|
||||||
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath)
|
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -256,7 +397,7 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string,
|
|||||||
return &buf, nil
|
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
|
// custom template function to fetch secrets from Infisical
|
||||||
decoded, err := base64.StdEncoding.DecodeString(encodedTemplate)
|
decoded, err := base64.StdEncoding.DecodeString(encodedTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -266,8 +407,10 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
|
|||||||
templateString := string(decoded)
|
templateString := string(decoded)
|
||||||
|
|
||||||
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
|
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
|
||||||
|
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"secret": secretFunction,
|
"secret": secretFunction,
|
||||||
|
"dynamic_secret": dynamicSecretFunction,
|
||||||
}
|
}
|
||||||
|
|
||||||
templateName := "base64Template"
|
templateName := "base64Template"
|
||||||
@@ -285,7 +428,7 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
|
|||||||
return &buf, nil
|
return &buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenManager struct {
|
type AgentManager struct {
|
||||||
accessToken string
|
accessToken string
|
||||||
accessTokenTTL time.Duration
|
accessTokenTTL time.Duration
|
||||||
accessTokenMaxTTL time.Duration
|
accessTokenMaxTTL time.Duration
|
||||||
@@ -294,6 +437,7 @@ type TokenManager struct {
|
|||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
filePaths []Sink // Store file paths if needed
|
filePaths []Sink // Store file paths if needed
|
||||||
templates []Template
|
templates []Template
|
||||||
|
dynamicSecretLeases *DynamicSecretLeaseManager
|
||||||
clientIdPath string
|
clientIdPath string
|
||||||
clientSecretPath string
|
clientSecretPath string
|
||||||
newAccessTokenNotificationChan chan bool
|
newAccessTokenNotificationChan chan bool
|
||||||
@@ -302,8 +446,8 @@ type TokenManager struct {
|
|||||||
exitAfterAuth bool
|
exitAfterAuth bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *TokenManager {
|
func NewAgentManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *AgentManager {
|
||||||
return &TokenManager{
|
return &AgentManager{
|
||||||
filePaths: fileDeposits,
|
filePaths: fileDeposits,
|
||||||
templates: templates,
|
templates: templates,
|
||||||
clientIdPath: clientIdPath,
|
clientIdPath: clientIdPath,
|
||||||
@@ -315,7 +459,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()
|
tm.mutex.Lock()
|
||||||
defer tm.mutex.Unlock()
|
defer tm.mutex.Unlock()
|
||||||
|
|
||||||
@@ -326,7 +470,7 @@ func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, acc
|
|||||||
tm.newAccessTokenNotificationChan <- true
|
tm.newAccessTokenNotificationChan <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TokenManager) GetToken() string {
|
func (tm *AgentManager) GetToken() string {
|
||||||
tm.mutex.Lock()
|
tm.mutex.Lock()
|
||||||
defer tm.mutex.Unlock()
|
defer tm.mutex.Unlock()
|
||||||
|
|
||||||
@@ -334,7 +478,7 @@ func (tm *TokenManager) GetToken() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetches a new access token using client credentials
|
// Fetches a new access token using client credentials
|
||||||
func (tm *TokenManager) FetchNewAccessToken() error {
|
func (tm *AgentManager) FetchNewAccessToken() error {
|
||||||
clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")
|
clientID := os.Getenv("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")
|
||||||
if clientID == "" {
|
if clientID == "" {
|
||||||
clientIDAsByte, err := ReadFile(tm.clientIdPath)
|
clientIDAsByte, err := ReadFile(tm.clientIdPath)
|
||||||
@@ -384,7 +528,7 @@ func (tm *TokenManager) FetchNewAccessToken() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refreshes the existing access token
|
// Refreshes the existing access token
|
||||||
func (tm *TokenManager) RefreshAccessToken() error {
|
func (tm *AgentManager) RefreshAccessToken() error {
|
||||||
httpClient := resty.New()
|
httpClient := resty.New()
|
||||||
httpClient.SetRetryCount(10000).
|
httpClient.SetRetryCount(10000).
|
||||||
SetRetryMaxWaitTime(20 * time.Second).
|
SetRetryMaxWaitTime(20 * time.Second).
|
||||||
@@ -405,7 +549,7 @@ func (tm *TokenManager) RefreshAccessToken() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TokenManager) ManageTokenLifecycle() {
|
func (tm *AgentManager) ManageTokenLifecycle() {
|
||||||
for {
|
for {
|
||||||
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
|
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
|
||||||
accessTokenRefreshedTime := tm.accessTokenRefreshedTime
|
accessTokenRefreshedTime := tm.accessTokenRefreshedTime
|
||||||
@@ -473,7 +617,7 @@ func (tm *TokenManager) ManageTokenLifecycle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TokenManager) WriteTokenToFiles() {
|
func (tm *AgentManager) WriteTokenToFiles() {
|
||||||
token := tm.GetToken()
|
token := tm.GetToken()
|
||||||
for _, sinkFile := range tm.filePaths {
|
for _, sinkFile := range tm.filePaths {
|
||||||
if sinkFile.Type == "file" {
|
if sinkFile.Type == "file" {
|
||||||
@@ -490,7 +634,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 {
|
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)
|
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
|
||||||
return
|
return
|
||||||
@@ -498,7 +642,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)
|
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)
|
pollingInterval := time.Duration(5 * time.Minute)
|
||||||
|
|
||||||
@@ -523,47 +667,61 @@ func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan ch
|
|||||||
execCommand := secretTemplate.Config.Execute.Command
|
execCommand := secretTemplate.Config.Execute.Command
|
||||||
|
|
||||||
for {
|
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
|
if err != nil {
|
||||||
var err error
|
log.Error().Msgf("unable to process template because %v", err)
|
||||||
|
} else {
|
||||||
|
if (existingEtag != currentEtag) || firstRun {
|
||||||
|
|
||||||
if secretTemplate.SourcePath != "" {
|
tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
|
||||||
processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag)
|
existingEtag = currentEtag
|
||||||
} else {
|
|
||||||
processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if !firstRun && execCommand != "" {
|
||||||
log.Error().Msgf("unable to process template because %v", err)
|
log.Info().Msgf("executing command: %s", execCommand)
|
||||||
} else {
|
err := ExecuteCommandWithTimeout(execCommand, execTimeout)
|
||||||
if (existingEtag != currentEtag) || firstRun {
|
|
||||||
|
|
||||||
tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
|
if err != nil {
|
||||||
existingEtag = currentEtag
|
log.Error().Msgf("unable to execute command because %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !firstRun && execCommand != "" {
|
}
|
||||||
log.Info().Msgf("executing command: %s", execCommand)
|
if firstRun {
|
||||||
err := ExecuteCommandWithTimeout(execCommand, execTimeout)
|
firstRun = false
|
||||||
|
}
|
||||||
if err != nil {
|
|
||||||
log.Error().Msgf("unable to execute command because %v", err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
time.Sleep(waitTime)
|
||||||
firstRun = false
|
} 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,13 +803,14 @@ var agentCmd = &cobra.Command{
|
|||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
filePaths := agentConfig.Sinks
|
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()
|
go tm.ManageTokenLifecycle()
|
||||||
|
|
||||||
for i, template := range agentConfig.Templates {
|
for i, template := range agentConfig.Templates {
|
||||||
log.Info().Msgf("template engine started for template %v...", i+1)
|
log.Info().Msgf("template engine started for template %v...", i+1)
|
||||||
go tm.MonitorSecretChanges(template, sigChan)
|
go tm.MonitorSecretChanges(template, i, sigChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@@ -297,7 +297,6 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
||||||
WorkspaceID: workspaceFile.WorkspaceId,
|
WorkspaceID: workspaceFile.WorkspaceId,
|
||||||
Environment: environmentName,
|
Environment: environmentName,
|
||||||
SecretName: secret.PlainTextKey,
|
|
||||||
SecretValueCiphertext: secret.SecretValueCiphertext,
|
SecretValueCiphertext: secret.SecretValueCiphertext,
|
||||||
SecretValueIV: secret.SecretValueIV,
|
SecretValueIV: secret.SecretValueIV,
|
||||||
SecretValueTag: secret.SecretValueTag,
|
SecretValueTag: secret.SecretValueTag,
|
||||||
@@ -305,7 +304,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
|
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to process secret update request")
|
util.HandleError(err, "Unable to process secret update request")
|
||||||
return
|
return
|
||||||
@@ -419,7 +418,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse path flag")
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
@@ -477,7 +476,7 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "")
|
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To fetch all secrets")
|
util.HandleError(err, "To fetch all secrets")
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type UserCredentials struct {
|
type UserCredentials struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"`
|
||||||
@@ -40,6 +42,23 @@ type PlaintextSecretResult struct {
|
|||||||
Etag string
|
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 SingleFolder struct {
|
type SingleFolder struct {
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
@@ -195,6 +195,31 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
|
|||||||
}, nil
|
}, 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) {
|
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
|
||||||
if importedSecrets == nil {
|
if importedSecrets == nil {
|
||||||
return secrets, nil
|
return secrets, nil
|
||||||
|
4
docs/api-reference/endpoints/secret-tags/create.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/workspace/{projectId}/tags"
|
||||||
|
---
|
4
docs/api-reference/endpoints/secret-tags/delete.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/secret-tags/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/workspace/{projectId}/tags"
|
||||||
|
---
|
4
docs/api-reference/endpoints/secrets/attach-tags.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Attach tags"
|
||||||
|
openapi: "POST /api/v3/secrets/tags/{secretName}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/secrets/detach-tags.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Detach tags"
|
||||||
|
openapi: "DELETE /api/v3/secrets/tags/{secretName}"
|
||||||
|
---
|
@@ -1,37 +1,102 @@
|
|||||||
---
|
---
|
||||||
title: "MySQL/MariaDB"
|
title: "MySQL/MariaDB"
|
||||||
description: "Rotated database user password of a MySQL or MariaDB"
|
description: "How to rotate MySQL/MariaDB database 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.
|
||||||
|
|
||||||
<Warning>
|
|
||||||
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.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
## Working
|
## Prerequisite
|
||||||
|
|
||||||
1. User's has to create the two user's for Infisical to rotate and provide them required database access
|
1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||||
2. Infisical will connect with your database with admin access
|
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.
|
||||||
3. If last rotated one was username1, then username2 is chosen to be rotated
|
|
||||||
5. Update it's password with random value
|
To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html).
|
||||||
6. After testing it gets saved to the provided secret mapping
|
|
||||||
|
## 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
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `MySQL`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin Username: DB admin username
|
</Step>
|
||||||
- Admin Password: DB admin password
|
<Step title="Click on MySQL card" />
|
||||||
- Host: DB host
|
<Step title="Provide the inputs">
|
||||||
- Port: DB port(number)
|
<ParamField path="Admin Username" type="string" required>
|
||||||
- Username1: The first username in two to rotate
|
Rotator admin username
|
||||||
- Username2: The second username in two to rotate
|
</ParamField>
|
||||||
- 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.
|
|
||||||
|
|
||||||
Congrats. You have 10x your MySQL/MariaDB access security.
|
<ParamField path="Admin password" type="string" required>
|
||||||
|
Rotator admin password
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Host" type="string" required>
|
||||||
|
Database host url
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Port" type="number" required>
|
||||||
|
Database port number
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username1" type="string" required>
|
||||||
|
The first username of two to rotate - `user-a`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username2" type="string" required>
|
||||||
|
The second username of two to rotate - `user-b`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="string">
|
||||||
|
Optional database certificate to connect with database
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB USERNAME" type="string" required>
|
||||||
|
Select an existing secret key where the rotated database username value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB PASSWORD" type="string" required>
|
||||||
|
Select an existing select key where the rotated database password value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Why can't we delete the other user when rotating?">
|
||||||
|
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.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Why do you need root user account?">
|
||||||
|
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.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
@@ -1,33 +1,104 @@
|
|||||||
---
|
---
|
||||||
title: "PostgreSQL/CockroachDB"
|
title: "PostgreSQL/CockroachDB"
|
||||||
description: "Rotated database user password of a PostgreSQL or Cockroach DB"
|
description: "How to rotate postgreSQL/cockroach database 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.
|
## Prerequisite
|
||||||
2. Infisical will connect with your database with admin access.
|
|
||||||
3. If last rotated one was username1, then username2 is chosen to be rotated.
|
1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||||
5. Update it's password with random value.
|
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.
|
||||||
6. After testing it gets saved to the provided secret mapping.
|
|
||||||
|
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
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `PostgreSQL`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin Username: DB admin username
|
</Step>
|
||||||
- Admin Password: DB admin password
|
<Step title="Click on PostgresSQL card" />
|
||||||
- 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.
|
|
||||||
|
|
||||||
Congratulations. You have improved your PostgreSQL/CockroachDB access security.
|
<Step title="Provide the inputs">
|
||||||
|
<ParamField path="Admin Username" type="string" required="true">
|
||||||
|
Rotator admin username
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Admin password" type="string" required="true">
|
||||||
|
Rotator admin password
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Host" type="string" required="true">
|
||||||
|
Database host url
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Port" type="number" required="true">
|
||||||
|
Database port number
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username1" type="string" required="true">
|
||||||
|
The first username of two to rotate - `user-a`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username2" type="string" required="true">
|
||||||
|
The second username of two to rotate - `user-b`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="string" optional>
|
||||||
|
Optional database certificate to connect with database
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB USERNAME" type="string" required>
|
||||||
|
Select an existing secret key where the rotated database username value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB PASSWORD" type="string" required>
|
||||||
|
Select an existing select key where the rotated database password value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Why can't we delete the other user when rotating?">
|
||||||
|
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.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Why do you need root user account?">
|
||||||
|
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.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
@@ -1,31 +1,58 @@
|
|||||||
---
|
---
|
||||||
title: "Twilio SendGrid"
|
title: "Twilio SendGrid"
|
||||||
description: "Rotate Twilio SendGrid API keys"
|
description: "How to rotate Twilio SendGrid API keys"
|
||||||
---
|
---
|
||||||
|
|
||||||
Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails.
|
Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical.
|
||||||
It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys.
|
|
||||||
|
|
||||||
## Working
|
## Prerequisite
|
||||||
|
|
||||||
1. Infisical will need an admin token of SendGrid to create API keys dynamically.
|
You will need a valid SendGrid admin key with the necessary scope to create additional API keys.
|
||||||
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)
|
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
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `Twilio SendGrid Card`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin API Key:
|
</Step>
|
||||||
SendGrid admin key to create lower scoped API keys.
|
<Step title="Click on Twilio SendGrid Card" />
|
||||||
- API Key Scopes
|
<Step title="Provide the inputs">
|
||||||
SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions)
|
<ParamField path="Admin API Key" type="string" required>
|
||||||
|
SendGrid admin API key with permission to create child scoped API keys.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
4. Final step
|
<ParamField path="Admin API Key" type="array" required>
|
||||||
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
|
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).
|
||||||
- Finally select the secrets in your provided board to replace with new secret after each rotation
|
Permissions must be entered as a list of strings.
|
||||||
- Your done and good to go.
|
|
||||||
|
|
||||||
Now your output mapped secret value will be replaced periodically by SendGrid.
|
|
||||||
|
|
||||||
|
Example: `["user.profile.read", "user.profile.update"]`
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="API KEY" type="string" required>
|
||||||
|
Select an existing select key where the newly rotated API key will get saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
Now your output mapped secret value will be replaced periodically by SendGrid.
|
||||||
|
Before Width: | Height: | Size: 733 KiB After Width: | Height: | Size: 739 KiB |
BIN
docs/images/secret-rotation/mysql-step1.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/images/secret-rotation/postgres-step1.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/images/secret-rotation/postgres-step2.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/images/secret-rotation/sendgrid-step1.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
docs/images/secret-rotation/sendgrid-step2.png
Normal file
After Width: | Height: | Size: 52 KiB |
@@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme
|
|||||||
|
|
||||||
## Install Operator
|
## 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)
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Helm (recommended)">
|
<Tab title="Helm (recommended)">
|
||||||
@@ -61,23 +61,38 @@ Once you have installed the operator to your cluster, you'll need to create a `I
|
|||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
kind: InfisicalSecret
|
kind: InfisicalSecret
|
||||||
metadata:
|
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:
|
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
|
||||||
hostAPI: https://app.infisical.com/api
|
resyncInterval: 10
|
||||||
resyncInterval: 60
|
authentication:
|
||||||
authentication:
|
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||||
serviceToken:
|
# If you have multiple authentication methods defined, it may cause issues.
|
||||||
serviceTokenSecretReference:
|
universalAuth:
|
||||||
secretName: service-token
|
secretsScope:
|
||||||
|
projectSlug: <project-slug>
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: "<secrets-path>" # Root is "/"
|
||||||
|
credentialsRef:
|
||||||
|
secretName: universal-auth-credentials
|
||||||
|
secretNamespace: default
|
||||||
|
|
||||||
|
serviceToken:
|
||||||
|
serviceTokenSecretReference:
|
||||||
|
secretName: service-token
|
||||||
|
secretNamespace: default
|
||||||
|
secretsScope:
|
||||||
|
envSlug: <env-slug>
|
||||||
|
secretsPath: <secrets-path> # Root is "/"
|
||||||
|
|
||||||
|
managedSecretReference:
|
||||||
|
secretName: managed-secret
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
secretsScope:
|
# secretType: kubernetes.io/dockerconfigjson
|
||||||
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
|
|
||||||
```
|
```
|
||||||
### InfisicalSecret CRD properties
|
### InfisicalSecret CRD properties
|
||||||
|
|
||||||
@@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute.
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication">
|
<Accordion title="authentication">
|
||||||
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
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication.serviceToken.serviceTokenSecretReference">
|
<Accordion title="authentication.universalAuth">
|
||||||
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.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create a machine identity">
|
||||||
|
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).
|
||||||
|
</Step>
|
||||||
|
<Step title="Create Kubernetes secret containing machine identity credentials">
|
||||||
|
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 `<your-identity-client-id>` with the identity client ID and `<your-identity-client-secret>` with the identity client secret.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
kubectl create secret generic universal-auth-credentials --from-literal=clientId="<your-identity-client-id>" --from-literal=clientSecret="<your-identity-client-secret>"
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add reference for the Kubernetes secret containing the identity credentials">
|
||||||
|
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.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
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.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```yaml
|
||||||
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
|
kind: InfisicalSecret
|
||||||
|
metadata:
|
||||||
|
name: infisicalsecret-sample-crd
|
||||||
|
spec:
|
||||||
|
authentication:
|
||||||
|
universalAuth:
|
||||||
|
secretsScope:
|
||||||
|
projectSlug: <project-slug> # <-- project slug
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: "<secrets-path>" # 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
|
||||||
|
...
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="authentication.serviceToken">
|
||||||
|
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.
|
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
|
#### 1. Generate service token
|
||||||
@@ -122,13 +186,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 `<your-service-token-here>` with your service token.
|
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here>
|
kubectl create secret generic service-token --from-literal=infisicalToken="<your-service-token-here>"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Add reference for the Kubernetes secret containing service token
|
#### 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.
|
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.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
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.
|
||||||
|
</Info>
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
@@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute.
|
|||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
|
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
|
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
|
||||||
|
secretsScope:
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: <secrets-path> # Root is "/"
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication.serviceToken.secretsScope">
|
|
||||||
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.
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="managedSecretReference">
|
<Accordion title="managedSecretReference">
|
||||||
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
|
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.
|
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
|
||||||
|
@@ -87,13 +87,23 @@ Since these encryption operations occur on the client-side, the Infisical API is
|
|||||||
|
|
||||||
### High availability
|
### 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.
|
#### Multi-AZ AWS RDS
|
||||||
- Storage backend: Bitnami MongoDB supports replica sets, which provide data redundancy and automatic failover for the underlying database.
|
Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments.
|
||||||
- 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.
|
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
|
### Snapshots
|
||||||
|
|
||||||
|
@@ -467,6 +467,14 @@
|
|||||||
"api-reference/endpoints/folders/delete"
|
"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",
|
"group": "Secrets",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -474,7 +482,9 @@
|
|||||||
"api-reference/endpoints/secrets/create",
|
"api-reference/endpoints/secrets/create",
|
||||||
"api-reference/endpoints/secrets/read",
|
"api-reference/endpoints/secrets/read",
|
||||||
"api-reference/endpoints/secrets/update",
|
"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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: "Using Infisical EE"
|
title: "Infisical enterprise"
|
||||||
description: "How to activate Infisical Enterprise Edition (EE) features"
|
description: "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.
|
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 in Infisical.
|
This guide walks through how you can use these paid features on a self hosted instance of Infisical.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Purchase a license">
|
<Step title="Purchase a license">
|
||||||
@@ -19,10 +19,11 @@ This guide walks through how you can use these paid features in Infisical.
|
|||||||
- 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 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.
|
- If using an offline license, you should set the value of the environment variable `LICENSE_KEY_OFFLINE` in Infisical to the issued license key.
|
||||||
|
|
||||||
Once your instance starts up, the license key will be validated and you’ll be able to use the paid features.
|
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Once 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.
|
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.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
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.
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
@@ -29,6 +29,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: ["react", "prettier", "simple-import-sort", "import"],
|
plugins: ["react", "prettier", "simple-import-sort", "import"],
|
||||||
rules: {
|
rules: {
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
quotes: ["error", "double", { avoidEscape: true }],
|
quotes: ["error", "double", { avoidEscape: true }],
|
||||||
"comma-dangle": ["error", "only-multiline"],
|
"comma-dangle": ["error", "only-multiline"],
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
@@ -72,7 +73,6 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
"simple-import-sort/exports": "warn",
|
"simple-import-sort/exports": "warn",
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"simple-import-sort/imports": [
|
"simple-import-sort/imports": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
@@ -1,28 +1,28 @@
|
|||||||
const path = require('path');
|
const path = require("path");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||||
addons: [
|
addons: [
|
||||||
'@storybook/addon-links',
|
"@storybook/addon-links",
|
||||||
'@storybook/addon-essentials',
|
"@storybook/addon-essentials",
|
||||||
'@storybook/addon-interactions',
|
"@storybook/addon-interactions",
|
||||||
'storybook-dark-mode',
|
"storybook-dark-mode",
|
||||||
{
|
{
|
||||||
name: '@storybook/addon-styling',
|
name: "@storybook/addon-styling",
|
||||||
options: {
|
options: {
|
||||||
postCss: {
|
postCss: {
|
||||||
implementation: require('postcss')
|
implementation: require("postcss")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
framework: {
|
framework: {
|
||||||
name: '@storybook/nextjs',
|
name: "@storybook/nextjs",
|
||||||
options: {}
|
options: {}
|
||||||
},
|
},
|
||||||
core: {
|
core: {
|
||||||
disableTelemetry: true
|
disableTelemetry: true
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
autodocs: 'tag'
|
autodocs: "tag"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
90
frontend/package-lock.json
generated
@@ -4,6 +4,7 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "frontend",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@casl/react": "^3.1.0",
|
"@casl/react": "^3.1.0",
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
|
"ms": "^2.1.3",
|
||||||
"next": "^12.3.4",
|
"next": "^12.3.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"picomatch": "^2.3.1",
|
"picomatch": "^2.3.1",
|
||||||
@@ -83,6 +85,7 @@
|
|||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
"set-cookie-parser": "^2.5.1",
|
"set-cookie-parser": "^2.5.1",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
@@ -9911,13 +9914,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.5",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"destroy": "1.2.0",
|
"destroy": "1.2.0",
|
||||||
@@ -9925,7 +9928,7 @@
|
|||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.11.0",
|
"qs": "6.11.0",
|
||||||
"raw-body": "2.5.1",
|
"raw-body": "2.5.2",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
},
|
},
|
||||||
@@ -10874,9 +10877,9 @@
|
|||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -11455,6 +11458,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": {
|
"node_modules/decode-named-character-reference": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
||||||
@@ -13255,17 +13263,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.18.2",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.1",
|
"body-parser": "1.20.2",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.5.0",
|
"cookie": "0.6.0",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@@ -13759,9 +13767,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.5",
|
"version": "1.15.6",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -15126,9 +15134,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ip": {
|
"node_modules/ip": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
@@ -17518,9 +17526,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
"node_modules/multipipe": {
|
"node_modules/multipipe": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -19786,9 +19794,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
@@ -20293,6 +20301,26 @@
|
|||||||
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -21287,12 +21315,6 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||||
@@ -23700,9 +23722,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-dev-middleware": {
|
"node_modules/webpack-dev-middleware": {
|
||||||
"version": "6.1.1",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz",
|
||||||
"integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==",
|
"integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colorette": "^2.0.10",
|
"colorette": "^2.0.10",
|
||||||
|
@@ -74,6 +74,7 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
|
"ms": "^2.1.3",
|
||||||
"next": "^12.3.4",
|
"next": "^12.3.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"picomatch": "^2.3.1",
|
"picomatch": "^2.3.1",
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"sanitize-html": "^2.12.1",
|
"sanitize-html": "^2.12.1",
|
||||||
"set-cookie-parser": "^2.5.1",
|
"set-cookie-parser": "^2.5.1",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
|
@@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config";
|
|||||||
|
|
||||||
export const initPostHog = () => {
|
export const initPostHog = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
console.log("Hi there 👋")
|
console.log("Hi there 👋");
|
||||||
try {
|
try {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -19,7 +19,7 @@ export const initPostHog = () => {
|
|||||||
|
|
||||||
return posthog;
|
return posthog;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("posthog err", e)
|
console.log("posthog err", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@@ -3,9 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
const Error = ({ text }: { text: string }): JSX.Element => {
|
const Error = ({ text }: { text: string }): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
|
<div className="relative m-auto flex w-fit flex-row items-center justify-center rounded-full">
|
||||||
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red mt-1.5 mb-2 mx-2" />
|
<FontAwesomeIcon icon={faExclamationTriangle} className="mx-2 mt-1.5 mb-2 text-red" />
|
||||||
{text && <p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>}
|
{text && <p className="relative top-0 mr-2 py-1 text-sm text-red">{text}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -39,16 +39,16 @@ const InputField = ({
|
|||||||
|
|
||||||
if (isStatic === true) {
|
if (isStatic === true) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md">
|
<div className="my-2 flex w-full max-w-md flex-col justify-center md:my-4">
|
||||||
<p className="text-sm font-semibold text-gray-400 mb-0.5">{label}</p>
|
<p className="mb-0.5 text-sm font-semibold text-gray-400">{label}</p>
|
||||||
{text && <p className="text-xs text-gray-400 mb-2">{text}</p>}
|
{text && <p className="mb-2 text-xs text-gray-400">{text}</p>}
|
||||||
<input
|
<input
|
||||||
onChange={(e) => onChangeHandler(e.target.value)}
|
onChange={(e) => onChangeHandler(e.target.value)}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
required={isRequired}
|
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}
|
name={name}
|
||||||
readOnly
|
readOnly
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
@@ -58,12 +58,12 @@ const InputField = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex-col w-full">
|
<div className="w-full flex-col">
|
||||||
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
|
<div className="mb-0.5 flex flex-row items-center text-mineshaft-300">
|
||||||
<p className="text-sm font-semibold mr-1">{label}</p>
|
<p className="mr-1 text-sm font-semibold">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
className={`group relative flex w-full max-w-2xl flex-col justify-center border ${
|
||||||
error ? "border-red" : "border-mineshaft-500"
|
error ? "border-red" : "border-mineshaft-500"
|
||||||
} rounded-md`}
|
} rounded-md`}
|
||||||
>
|
>
|
||||||
@@ -75,11 +75,11 @@ const InputField = ({
|
|||||||
required={isRequired}
|
required={isRequired}
|
||||||
className={`${
|
className={`${
|
||||||
blurred
|
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"
|
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}
|
name={name}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
@@ -91,7 +91,7 @@ const InputField = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPasswordVisible(!passwordVisible);
|
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 ? (
|
{passwordVisible ? (
|
||||||
<FontAwesomeIcon icon={faEyeSlash} />
|
<FontAwesomeIcon icon={faEyeSlash} />
|
||||||
@@ -101,7 +101,7 @@ const InputField = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{blurred && (
|
{blurred && (
|
||||||
<div className="peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden">
|
<div className="peer absolute flex h-10 w-fit max-w-xl items-center overflow-hidden text-clip rounded-md text-gray-400/50 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible">
|
||||||
<p className="ml-2" />
|
<p className="ml-2" />
|
||||||
{value
|
{value
|
||||||
.split("")
|
.split("")
|
||||||
@@ -109,7 +109,7 @@ const InputField = ({
|
|||||||
.map(() => (
|
.map(() => (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
key={guidGenerator()}
|
key={guidGenerator()}
|
||||||
className="text-xxs mx-0.5"
|
className="mx-0.5 text-xxs"
|
||||||
icon={faCircle}
|
icon={faCircle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -121,7 +121,7 @@ const InputField = ({
|
|||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs">{errorText}</p>}
|
{error && <p className="mx-0 mt-0.5 mb-2 max-w-xs text-xs text-red">{errorText}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -34,19 +34,19 @@ const ListBox = ({
|
|||||||
<Listbox value={isSelected} onChange={onChange}>
|
<Listbox value={isSelected} onChange={onChange}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={`text-gray-400 relative ${
|
className={`relative text-gray-400 ${
|
||||||
isFull ? "w-full" : "w-52"
|
isFull ? "w-full" : "w-52"
|
||||||
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
|
} focus-visible:ring-offset-orange-300 cursor-default rounded-md bg-white/[0.07] py-2.5 pl-3 pr-10 text-left shadow-md duration-200 hover:bg-white/[0.11] focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{text}
|
{text}
|
||||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
|
<span className="ml-1 block cursor-pointer truncate font-semibold text-gray-300">
|
||||||
{" "}
|
{" "}
|
||||||
{isSelected}
|
{isSelected}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{data && (
|
{data && (
|
||||||
<div className="cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex cursor-pointer items-center pr-2">
|
||||||
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -58,16 +58,16 @@ const ListBox = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="border border-mineshaft-700 z-[70] p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm no-scrollbar no-scrollbar::-webkit-scrollbar">
|
<Listbox.Options className="no-scrollbar::-webkit-scrollbar absolute z-[70] mt-1 max-h-60 w-full overflow-auto rounded-md border border-mineshaft-700 bg-bunker p-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 no-scrollbar focus:outline-none sm:text-sm">
|
||||||
{data.map((person, personIdx) => (
|
{data.map((person, personIdx) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={`${person}.${personIdx + 1}`}
|
key={`${person}.${personIdx + 1}`}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
|
`relative my-0.5 cursor-default select-none rounded-md py-2 pl-10 pr-4 ${
|
||||||
selected ? "bg-white/10 text-gray-400 font-bold" : ""
|
selected ? "bg-white/10 font-bold text-gray-400" : ""
|
||||||
} ${
|
} ${
|
||||||
active && !selected
|
active && !selected
|
||||||
? "bg-white/5 text-mineshaft-200 cursor-pointer"
|
? "cursor-pointer bg-white/5 text-mineshaft-200"
|
||||||
: "text-gray-400"
|
: "text-gray-400"
|
||||||
} `
|
} `
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ const ListBox = ({
|
|||||||
{person}
|
{person}
|
||||||
</span>
|
</span>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3">
|
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 text-primary">
|
||||||
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -92,9 +92,9 @@ const ListBox = ({
|
|||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
{buttonAction && (
|
{buttonAction && (
|
||||||
<button type="button" onClick={buttonAction} className="cursor-pointer w-full">
|
<button type="button" onClick={buttonAction} className="w-full cursor-pointer">
|
||||||
<div className="my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2">
|
<div className="relative my-0.5 mt-2 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-lime-300 hover:font-semibold hover:text-black">
|
||||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
|
||||||
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
||||||
</span>
|
</span>
|
||||||
Add Project
|
Add Project
|
||||||
|
@@ -43,7 +43,7 @@ const Button = ({
|
|||||||
loading,
|
loading,
|
||||||
icon,
|
icon,
|
||||||
iconDisabled,
|
iconDisabled,
|
||||||
type = "button",
|
type = "button"
|
||||||
}: ButtonProps): JSX.Element => {
|
}: ButtonProps): JSX.Element => {
|
||||||
// Check if the button show always be 'active' - then true;
|
// Check if the button show always be 'active' - then true;
|
||||||
// or if it should switch between 'active' and 'disabled' - then give the status
|
// or if it should switch between 'active' and 'disabled' - then give the status
|
||||||
@@ -53,9 +53,13 @@ const Button = ({
|
|||||||
"group m-auto md:m-0 inline-block rounded-md duration-200",
|
"group m-auto md:m-0 inline-block rounded-md duration-200",
|
||||||
|
|
||||||
// Setting background colors and hover modes
|
// Setting background colors and hover modes
|
||||||
color === "mineshaft" && activityStatus && "bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60",
|
color === "mineshaft" &&
|
||||||
|
activityStatus &&
|
||||||
|
"bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60",
|
||||||
color === "mineshaft" && !activityStatus && "bg-mineshaft",
|
color === "mineshaft" && !activityStatus && "bg-mineshaft",
|
||||||
(color === "primary" || !color) && activityStatus && "bg-primary border border-primary-400 opacity-80 hover:opacity-100",
|
(color === "primary" || !color) &&
|
||||||
|
activityStatus &&
|
||||||
|
"bg-primary border border-primary-400 opacity-80 hover:opacity-100",
|
||||||
(color === "primary" || !color) && !activityStatus && "bg-primary",
|
(color === "primary" || !color) && !activityStatus && "bg-primary",
|
||||||
color === "red" && "bg-red-800 border border-red",
|
color === "red" && "bg-red-800 border border-red",
|
||||||
|
|
||||||
@@ -78,7 +82,9 @@ const Button = ({
|
|||||||
color !== "mineshaft" && color !== "red" && color !== "none" && "text-black",
|
color !== "mineshaft" && color !== "red" && color !== "none" && "text-black",
|
||||||
color === "red" && "text-gray-200",
|
color === "red" && "text-gray-200",
|
||||||
color === "none" && "text-gray-200 text-xl",
|
color === "none" && "text-gray-200 text-xl",
|
||||||
activityStatus && color !== "red" && color !== "mineshaft" && color !== "none" ? "group-hover:text-black" : "",
|
activityStatus && color !== "red" && color !== "mineshaft" && color !== "none"
|
||||||
|
? "group-hover:text-black"
|
||||||
|
: "",
|
||||||
|
|
||||||
size === "icon" && "flex items-center justify-center"
|
size === "icon" && "flex items-center justify-center"
|
||||||
);
|
);
|
||||||
@@ -103,7 +109,7 @@ const Button = ({
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
loading === true ? "opacity-100" : "opacity-0"
|
loading === true ? "opacity-100" : "opacity-0"
|
||||||
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
} absolute flex w-full items-center bg-primary px-3 duration-200`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loadingblack.gif"
|
src="/images/loading/loadingblack.gif"
|
||||||
@@ -116,7 +122,7 @@ const Button = ({
|
|||||||
{icon && (
|
{icon && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={`flex my-auto font-extrabold ${size === "icon-sm" ? "text-sm" : "text-sm"} ${
|
className={`my-auto flex font-extrabold ${size === "icon-sm" ? "text-sm" : "text-sm"} ${
|
||||||
(text || textDisabled) && "mr-2"
|
(text || textDisabled) && "mr-2"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -124,7 +130,7 @@ const Button = ({
|
|||||||
{iconDisabled && (
|
{iconDisabled && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={iconDisabled as IconProp}
|
icon={iconDisabled as IconProp}
|
||||||
className={`flex my-auto font-extrabold ${size === "icon-sm" ? "text-sm" : "text-md"} ${
|
className={`my-auto flex font-extrabold ${size === "icon-sm" ? "text-sm" : "text-md"} ${
|
||||||
(text || textDisabled) && "mr-2"
|
(text || textDisabled) && "mr-2"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
@@ -64,7 +64,7 @@ const AddProjectMemberDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as="h3"
|
||||||
className="z-50 text-lg font-medium text-mineshaft-300 mb-4"
|
className="z-50 mb-4 text-lg font-medium text-mineshaft-300"
|
||||||
>
|
>
|
||||||
{t("section.members.add-dialog.already-all-invited")}
|
{t("section.members.add-dialog.already-all-invited")}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
@@ -127,7 +127,9 @@ const AddProjectMemberDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => router.push(`/org/${localStorage.getItem("orgData.id")}/members`)}
|
onButtonPressed={() =>
|
||||||
|
router.push(`/org/${localStorage.getItem("orgData.id")}/members`)
|
||||||
|
}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
text={t("section.members.add-dialog.add-user-to-org") as string}
|
text={t("section.members.add-dialog.add-user-to-org") as string}
|
||||||
size="md"
|
size="md"
|
||||||
|
@@ -28,11 +28,11 @@ export const AddUpdateEnvironmentDialog = ({
|
|||||||
onCreateSubmit,
|
onCreateSubmit,
|
||||||
onEditSubmit,
|
onEditSubmit,
|
||||||
initialValues,
|
initialValues,
|
||||||
isEditMode,
|
isEditMode
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [formInput, setFormInput] = useState<FormFields>({
|
const [formInput, setFormInput] = useState<FormFields>({
|
||||||
name: "",
|
name: "",
|
||||||
slug: "",
|
slug: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
// This use effect can be removed when the unmount is happening from outside the component
|
// This use effect can be removed when the unmount is happening from outside the component
|
||||||
@@ -50,7 +50,7 @@ export const AddUpdateEnvironmentDialog = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {
|
const data = {
|
||||||
name: formInput.name,
|
name: formInput.name,
|
||||||
slug: formInput.slug.toLowerCase(),
|
slug: formInput.slug.toLowerCase()
|
||||||
};
|
};
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
onEditSubmit(data);
|
onEditSubmit(data);
|
||||||
@@ -62,75 +62,70 @@ export const AddUpdateEnvironmentDialog = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative z-20' onClose={onClose}>
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-out duration-150'
|
leave="ease-out duration-150"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className='fixed inset-0 overflow-y-auto z-50'>
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as='h3'
|
{isEditMode ? "Update environment" : "Create a new environment"}
|
||||||
className='text-lg font-medium leading-6 text-gray-400'
|
|
||||||
>
|
|
||||||
{isEditMode
|
|
||||||
? "Update environment"
|
|
||||||
: "Create a new environment"}
|
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<form onSubmit={onFormSubmit}>
|
<form onSubmit={onFormSubmit}>
|
||||||
<div className='max-h-28 mt-4'>
|
<div className="mt-4 max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label='Environment Name'
|
label="Environment Name"
|
||||||
onChangeHandler={(val) => onInputChange("name", val)}
|
onChangeHandler={(val) => onInputChange("name", val)}
|
||||||
type='varName'
|
type="varName"
|
||||||
value={formInput.name}
|
value={formInput.name}
|
||||||
placeholder=''
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
// error={error.length > 0}
|
// error={error.length > 0}
|
||||||
// errorText={error}
|
// errorText={error}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='max-h-28 mt-4'>
|
<div className="mt-4 max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label='Environment Slug'
|
label="Environment Slug"
|
||||||
onChangeHandler={(val) => onInputChange("slug", val)}
|
onChangeHandler={(val) => onInputChange("slug", val)}
|
||||||
type='varName'
|
type="varName"
|
||||||
value={formInput.slug}
|
value={formInput.slug}
|
||||||
placeholder=''
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
// error={error.length > 0}
|
// error={error.length > 0}
|
||||||
// errorText={error}
|
// errorText={error}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-xs text-gray-500 mt-2'>
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
Slugs are shorthands used in cli to access environment
|
Slugs are shorthands used in cli to access environment
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-4 max-w-min'>
|
<div className="mt-4 max-w-min">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => null}
|
onButtonPressed={() => null}
|
||||||
type='submit'
|
type="submit"
|
||||||
color='mineshaft'
|
color="mineshaft"
|
||||||
text={isEditMode ? "Update" : "Create"}
|
text={isEditMode ? "Update" : "Create"}
|
||||||
active={formInput.name !== "" && formInput.slug !== ""}
|
active={formInput.name !== "" && formInput.slug !== ""}
|
||||||
size='md'
|
size="md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -13,76 +13,63 @@ type Props = {
|
|||||||
orgName: string;
|
orgName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddUserDialog = ({
|
const AddUserDialog = ({ isOpen, closeModal, submitModal, email, setEmail, orgName }: Props) => {
|
||||||
isOpen,
|
|
||||||
closeModal,
|
|
||||||
submitModal,
|
|
||||||
email,
|
|
||||||
setEmail,
|
|
||||||
orgName,
|
|
||||||
}: Props) => {
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
submitModal(email);
|
submitModal(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='z-50'>
|
<div className="z-50">
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-lg transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as='h3'
|
as="h3"
|
||||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||||
>
|
>
|
||||||
Invite others to {orgName}
|
Invite others to {orgName}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2 mb-4'>
|
<div className="mt-2 mb-4">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">
|
||||||
An invite is specific to an email address and expires
|
An invite is specific to an email address and expires after 1 day. For
|
||||||
after 1 day. For security reasons, you will need to
|
security reasons, you will need to separately add members to projects.
|
||||||
separately add members to projects.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='max-h-28'>
|
<div className="max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label='Email'
|
label="Email"
|
||||||
onChangeHandler={setEmail}
|
onChangeHandler={setEmail}
|
||||||
type='varName'
|
type="varName"
|
||||||
value={email}
|
value={email}
|
||||||
placeholder=''
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-4 max-w-max'>
|
<div className="mt-4 max-w-max">
|
||||||
<Button
|
<Button onButtonPressed={submit} color="mineshaft" text="Invite" size="md" />
|
||||||
onButtonPressed={submit}
|
|
||||||
color='mineshaft'
|
|
||||||
text='Invite'
|
|
||||||
size='md'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
|
@@ -5,7 +5,6 @@ import Button from "../buttons/Button";
|
|||||||
import InputField from "../InputField";
|
import InputField from "../InputField";
|
||||||
import { Checkbox } from "../table/Checkbox";
|
import { Checkbox } from "../table/Checkbox";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
@@ -26,8 +25,8 @@ const AddWorkspaceDialog = ({
|
|||||||
workspaceName,
|
workspaceName,
|
||||||
setWorkspaceName,
|
setWorkspaceName,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading
|
||||||
}:Props) => {
|
}: Props) => {
|
||||||
const [addAllUsers, setAddAllUsers] = useState(true);
|
const [addAllUsers, setAddAllUsers] = useState(true);
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
submitModal(workspaceName, addAllUsers);
|
submitModal(workspaceName, addAllUsers);
|
||||||
@@ -60,11 +59,8 @@ const AddWorkspaceDialog = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-gray-400"
|
|
||||||
>
|
|
||||||
Create a new project
|
Create a new project
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -72,7 +68,7 @@ const AddWorkspaceDialog = ({
|
|||||||
This project will contain your secrets and configs.
|
This project will contain your secrets and configs.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28 mt-4">
|
<div className="mt-4 max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label="Project Name"
|
label="Project Name"
|
||||||
onChangeHandler={setWorkspaceName}
|
onChangeHandler={setWorkspaceName}
|
||||||
@@ -84,10 +80,7 @@ const AddWorkspaceDialog = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 ml-1">
|
<div className="mt-4 ml-1">
|
||||||
<Checkbox
|
<Checkbox addAllUsers={addAllUsers} setAddAllUsers={setAddAllUsers} />
|
||||||
addAllUsers={addAllUsers}
|
|
||||||
setAddAllUsers={setAddAllUsers}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 max-w-min">
|
<div className="mt-4 max-w-min">
|
||||||
<Button
|
<Button
|
||||||
|
@@ -3,89 +3,76 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
|
|
||||||
import InputField from "../InputField";
|
import InputField from "../InputField";
|
||||||
|
|
||||||
// REFACTOR: Move all these modals into one reusable one
|
// REFACTOR: Move all these modals into one reusable one
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
onClose: ()=>void;
|
onClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
onSubmit:()=>void;
|
onSubmit: () => void;
|
||||||
deleteKey?:string;
|
deleteKey?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const DeleteActionModal = ({
|
const DeleteActionModal = ({ isOpen, onClose, title, onSubmit, deleteKey }: Props) => {
|
||||||
isOpen,
|
const [deleteInputField, setDeleteInputField] = useState("");
|
||||||
onClose,
|
|
||||||
title,
|
|
||||||
onSubmit,
|
|
||||||
deleteKey
|
|
||||||
}:Props) => {
|
|
||||||
const [deleteInputField, setDeleteInputField] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDeleteInputField("");
|
setDeleteInputField("");
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-150'
|
leave="ease-in duration-150"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-grey p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as='h3'
|
|
||||||
className='text-lg font-medium leading-6 text-gray-400'
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2'>
|
<div className="mt-2">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">This action is irrevertible.</p>
|
||||||
This action is irrevertible.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2'>
|
<div className="mt-2">
|
||||||
<InputField
|
<InputField
|
||||||
isRequired
|
isRequired
|
||||||
label={`Type ${deleteKey} to delete the resource`}
|
label={`Type ${deleteKey} to delete the resource`}
|
||||||
onChangeHandler={(val) => setDeleteInputField(val)}
|
onChangeHandler={(val) => setDeleteInputField(val)}
|
||||||
value={deleteInputField}
|
value={deleteInputField}
|
||||||
type='text'
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:bg-alizarin hover:text-semibold inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
disabled={
|
disabled={Boolean(deleteKey) && deleteInputField !== deleteKey}
|
||||||
Boolean(deleteKey) && deleteInputField !== deleteKey
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:border-white hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
@@ -5,13 +5,13 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
onSubmit: () => void
|
onSubmit: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
@@ -45,7 +45,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-mineshaft-600 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-mineshaft-600 bg-bunker p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
|
||||||
{t("dashboard:sidebar.delete-key-dialog.title")}
|
{t("dashboard:sidebar.delete-key-dialog.title")}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
@@ -57,14 +57,14 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
|||||||
<div className="mt-6 flex justify-start">
|
<div className="mt-6 flex justify-start">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex justify-center rounded-md border border-transparent bg-red-500 opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium text-bunker-100 text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
className="text-semibold inline-flex justify-center rounded-md border border-transparent bg-red-500 px-4 py-2 text-sm font-medium text-bunker-100 opacity-80 duration-200 hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-mineshaft-500 hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:bg-mineshaft-500 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
@@ -10,66 +10,55 @@ type Props = {
|
|||||||
userIdToBeDeleted: string;
|
userIdToBeDeleted: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteUserDialog = ({
|
const DeleteUserDialog = ({ isOpen, closeModal, submitModal, userIdToBeDeleted }: Props) => {
|
||||||
isOpen,
|
|
||||||
closeModal,
|
|
||||||
submitModal,
|
|
||||||
userIdToBeDeleted,
|
|
||||||
}: Props) => {
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
submitModal(userIdToBeDeleted);
|
submitModal(userIdToBeDeleted);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative z-10' onClose={closeModal}>
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl border border-gray-700 bg-grey p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as='h3'
|
Are you sure you want to remove this user from the workspace?
|
||||||
className='text-lg font-medium leading-6 text-gray-400'
|
|
||||||
>
|
|
||||||
Are you sure you want to remove this user from the
|
|
||||||
workspace?
|
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2'>
|
<div className="mt-2">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">This action is irrevertible.</p>
|
||||||
This action is irrevertible.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-6'>
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:bg-alizarin hover:text-semibold inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="hover:text-semibold ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 duration-200 hover:border-white hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
@@ -34,27 +34,27 @@ const BottonRightPopup = ({
|
|||||||
}: PopupProps): JSX.Element => {
|
}: PopupProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="z-[100] drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
className="absolute bottom-0 right-0 z-[100] mr-6 mb-6 flex max-w-xl flex-col items-start rounded-md border border-gray-600/50 bg-bunker pt-3 pb-4 text-gray-200 drop-shadow-xl"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
<div className="flex w-full flex-row items-center justify-between border-b border-gray-600/70 px-6 pb-3">
|
||||||
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
<div className="mr-2 mt-0.5 flex flex-row text-xl font-bold">
|
||||||
<div>{titleText}</div>
|
<div>{titleText}</div>
|
||||||
<div className="ml-2.5">{emoji}</div>
|
<div className="ml-2.5">{emoji}</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
|
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faXmark}
|
icon={faXmark}
|
||||||
className="text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer"
|
className="cursor-pointer text-2xl text-gray-400 duration-200 hover:text-red"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">{textLine1}</div>
|
<div className="mt-4 mb-0.5 block px-6 text-gray-300 sm:inline">{textLine1}</div>
|
||||||
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
<div className="mb-4 block px-6 sm:inline">{textLine2}</div>
|
||||||
<div className="flex flex-row px-6 w-full">
|
<div className="flex w-full flex-row px-6">
|
||||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||||
<a
|
<a
|
||||||
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
className="flex w-full justify-center rounded-md bg-white/10 p-2 font-bold duration-200 hover:bg-primary hover:text-black"
|
||||||
href={buttonLink}
|
href={buttonLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
|
@@ -9,7 +9,7 @@ export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
|
|||||||
{addAllUsers === true ? (
|
{addAllUsers === true ? (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="accent-primary h-4 w-4"
|
className="h-4 w-4 accent-primary"
|
||||||
checked
|
checked
|
||||||
readOnly
|
readOnly
|
||||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||||
@@ -20,12 +20,12 @@ export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="add all users"
|
aria-label="add all users"
|
||||||
className="h-4 w-4 bg-bunker border border-gray-600 rounded-sm"
|
className="h-4 w-4 rounded-sm border border-gray-600 bg-bunker"
|
||||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label className="ml-2 text-gray-500 text-sm">
|
<label className="ml-2 text-sm text-gray-500">
|
||||||
Add all members of my organization to this project.
|
Add all members of my organization to this project.
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,63 +0,0 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
type NotificationType = "success" | "error" | "info";
|
|
||||||
|
|
||||||
export type TNotification = {
|
|
||||||
text: string;
|
|
||||||
type?: NotificationType;
|
|
||||||
timeoutMs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface NotificationProps {
|
|
||||||
notification: Required<TNotification>;
|
|
||||||
clearNotification: (text: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Notification = ({ notification, clearNotification }: NotificationProps) => {
|
|
||||||
const timeout = useRef<number>();
|
|
||||||
|
|
||||||
const handleClearNotification = () => clearNotification(notification.text);
|
|
||||||
|
|
||||||
const setNotifTimeout = () => {
|
|
||||||
timeout.current = window.setTimeout(handleClearNotification, notification.timeoutMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelNotifTimeout = () => {
|
|
||||||
clearTimeout(timeout.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNotifTimeout();
|
|
||||||
|
|
||||||
return cancelNotifTimeout;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative w-full flex items-center justify-between px-6 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
{notification.type === "error" && (
|
|
||||||
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md" />
|
|
||||||
)}
|
|
||||||
{notification.type === "success" && (
|
|
||||||
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md" />
|
|
||||||
)}
|
|
||||||
{notification.type === "info" && (
|
|
||||||
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md" />
|
|
||||||
)}
|
|
||||||
<p className="text-bunker-200 text-md font-base mt-0.5">{notification.text}</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded-lg"
|
|
||||||
onClick={() => clearNotification(notification.text)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon className="absolute right-2 top-3 text-bunker-300 pl-2 w-4 h-4 hover:text-white" icon={faXmark} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Notification;
|
|
@@ -1,52 +0,0 @@
|
|||||||
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import { TNotification } from "./Notification";
|
|
||||||
import Notifications from "./Notifications";
|
|
||||||
|
|
||||||
type NotificationContextState = {
|
|
||||||
createNotification: (newNotification: TNotification) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NotificationContext = createContext<NotificationContextState>({
|
|
||||||
createNotification: () => console.log("createNotification not set!")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useNotificationContext = () => useContext(NotificationContext);
|
|
||||||
|
|
||||||
interface NotificationProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Migration to radix toast
|
|
||||||
const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
|
||||||
const [notifications, setNotifications] = useState<Required<TNotification>[]>([]);
|
|
||||||
|
|
||||||
const clearNotification = (text: string) =>
|
|
||||||
setNotifications((state) => state.filter((notif) => notif.text !== text));
|
|
||||||
|
|
||||||
const createNotification = useCallback(
|
|
||||||
({ text, type = "success", timeoutMs = 4000 }: TNotification) => {
|
|
||||||
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
|
||||||
|
|
||||||
if (doesNotifExist) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNotification: Required<TNotification> = { text, type, timeoutMs };
|
|
||||||
|
|
||||||
setNotifications((state) => [...state, newNotification]);
|
|
||||||
},
|
|
||||||
[notifications]
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = useMemo(() => ({ createNotification }), [createNotification]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationContext.Provider value={value}>
|
|
||||||
<Notifications notifications={notifications} clearNotification={clearNotification} />
|
|
||||||
{children}
|
|
||||||
</NotificationContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotificationProvider;
|
|
@@ -1,22 +0,0 @@
|
|||||||
import Notification, { TNotification } from "./Notification";
|
|
||||||
|
|
||||||
interface NoticationsProps {
|
|
||||||
notifications: Required<TNotification>[];
|
|
||||||
clearNotification: (text: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Notifications = ({ notifications, clearNotification }: NoticationsProps) => {
|
|
||||||
if (!notifications.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="hidden fixed z-50 md:flex md:flex-col-reverse gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
|
|
||||||
{notifications.map((notif) => (
|
|
||||||
<Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Notifications;
|
|
@@ -30,9 +30,9 @@ const ConfirmEnvOverwriteModal = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className='text-gray-400'>Your file contains the following duplicate secrets:</p>
|
<p className="text-gray-400">Your file contains the following duplicate secrets:</p>
|
||||||
<p className="text-sm text-gray-500">{duplicateKeys.join(", ")}</p>
|
<p className="text-sm text-gray-500">{duplicateKeys.join(", ")}</p>
|
||||||
<p className='text-md text-gray-400'>Are you sure you want to overwrite these secrets?</p>
|
<p className="text-md text-gray-400">Are you sure you want to overwrite these secrets?</p>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -1,5 +1,10 @@
|
|||||||
import { memo, SyntheticEvent, useRef } from "react";
|
import { memo, SyntheticEvent, useRef } from "react";
|
||||||
import { faCircle, faCodeBranch, faExclamationCircle, faEye } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCircle,
|
||||||
|
faCodeBranch,
|
||||||
|
faExclamationCircle,
|
||||||
|
faEye
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import guidGenerator from "../utilities/randomId";
|
import guidGenerator from "../utilities/randomId";
|
||||||
@@ -31,8 +36,8 @@ interface DashboardInputFieldProps {
|
|||||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||||
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||||
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -61,29 +66,31 @@ const DashboardInputField = ({
|
|||||||
const error = startsWithNumber || isDuplicate;
|
const error = startsWithNumber || isDuplicate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative flex-col w-full h-10 ${
|
<div
|
||||||
error && value !== "" ? "bg-red/[0.15]" : ""
|
className={`relative h-10 w-full flex-col ${error && value !== "" ? "bg-red/[0.15]" : ""} ${
|
||||||
} ${
|
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
||||||
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
}`}
|
||||||
}`}>
|
>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-col justify-center items-center h-full ${
|
className={`group relative flex h-full flex-col items-center justify-center ${
|
||||||
error ? "w-max" : "w-full"
|
error ? "w-max" : "w-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)}
|
onChange={(e) =>
|
||||||
|
onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)
|
||||||
|
}
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
|
className={`ph-no-capture min-w-16 peer z-10 h-full w-full bg-transparent px-2 font-mono text-sm caret-bunker-200 outline-none ${
|
||||||
error ? "text-red-600 focus:text-red-500" : "text-bunker-300 focus:text-bunker-100"
|
error ? "text-red-600 focus:text-red-500" : "text-bunker-300 focus:text-bunker-100"
|
||||||
} duration-200`}
|
} duration-200`}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{startsWithNumber && (
|
{startsWithNumber && (
|
||||||
<div className='absolute right-2 top-2 text-red z-50'>
|
<div className="absolute right-2 top-2 z-50 text-red">
|
||||||
<HoverObject
|
<HoverObject
|
||||||
text="Secret names should not start with a number"
|
text="Secret names should not start with a number"
|
||||||
icon={faExclamationCircle}
|
icon={faExclamationCircle}
|
||||||
color="red"
|
color="red"
|
||||||
@@ -91,33 +98,44 @@ const DashboardInputField = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isDuplicate && value !== "" && !startsWithNumber && (
|
{isDuplicate && value !== "" && !startsWithNumber && (
|
||||||
<div className='absolute right-2 top-2 text-red z-50'>
|
<div className="absolute right-2 top-2 z-50 text-red">
|
||||||
<HoverObject
|
<HoverObject
|
||||||
text="Secret names should be unique"
|
text="Secret names should be unique"
|
||||||
icon={faExclamationCircle}
|
icon={faExclamationCircle}
|
||||||
color="red"
|
color="red"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!error && <div className={`absolute right-0 top-0 text-red z-50 bg-mineshaft-800 group-hover:bg-mineshaft-700 ${
|
{!error && (
|
||||||
overrideEnabled ? "visible" : "invisible group-hover:visible"
|
<div
|
||||||
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
|
className={`absolute right-0 top-0 z-50 bg-mineshaft-800 text-red group-hover:bg-mineshaft-700 ${
|
||||||
<button type="button" onClick={() => {
|
overrideEnabled ? "visible" : "invisible group-hover:visible"
|
||||||
if (modifyValueOverride) {
|
} duration-0 flex h-10 cursor-pointer items-center px-2`}
|
||||||
if (overrideEnabled === false) {
|
>
|
||||||
modifyValueOverride("", id);
|
<button
|
||||||
} else {
|
type="button"
|
||||||
modifyValueOverride(undefined, id);
|
onClick={() => {
|
||||||
}
|
if (modifyValueOverride) {
|
||||||
}
|
if (overrideEnabled === false) {
|
||||||
}}>
|
modifyValueOverride("", id);
|
||||||
<HoverObject
|
} else {
|
||||||
text={overrideEnabled ? "This secret is overriden with your personal value" : "You can override this secret with a personal value"}
|
modifyValueOverride(undefined, id);
|
||||||
icon={faCodeBranch}
|
}
|
||||||
color={overrideEnabled ? "primary" : "bunker-400"}
|
}
|
||||||
/>
|
}}
|
||||||
</button>
|
>
|
||||||
</div>}
|
<HoverObject
|
||||||
|
text={
|
||||||
|
overrideEnabled
|
||||||
|
? "This secret is overriden with your personal value"
|
||||||
|
: "You can override this secret with a personal value"
|
||||||
|
}
|
||||||
|
icon={faCodeBranch}
|
||||||
|
color={overrideEnabled ? "primary" : "bunker-400"}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -127,20 +145,29 @@ const DashboardInputField = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverObject text={value || ""} onChangeHandler={onChangeHandler} id={id}>
|
<PopoverObject text={value || ""} onChangeHandler={onChangeHandler} id={id}>
|
||||||
<div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${
|
<div
|
||||||
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
title={value}
|
||||||
}`}>
|
className={`relative h-10 w-full flex-col overflow-hidden ${
|
||||||
|
isSideBarOpen && "bg-mineshaft-700 duration-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-col justify-center items-center h-full ${
|
className={`group relative flex h-full flex-col items-center justify-center ${
|
||||||
error ? "w-max" : "w-full"
|
error ? "w-max" : "w-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{value?.split("\n")[0] ? <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
|
{value?.split("\n")[0] ? (
|
||||||
{value?.split("\n")[0]}
|
<span className="ph-no-capture min-w-16 placeholder w-full truncate break-all bg-transparent px-2 text-xs leading-tight text-bunker-300 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent">
|
||||||
</span> : <span className='text-bunker-400'>-</span> }
|
{value?.split("\n")[0]}
|
||||||
{value?.split("\n")[1] && <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
|
</span>
|
||||||
{value?.split("\n")[1]}
|
) : (
|
||||||
</span>}
|
<span className="text-bunker-400">-</span>
|
||||||
|
)}
|
||||||
|
{value?.split("\n")[1] && (
|
||||||
|
<span className="ph-no-capture min-w-16 placeholder w-full truncate break-all bg-transparent px-2 text-xs leading-tight text-bunker-300 outline-none duration-200 placeholder:text-bunker-400 focus:text-bunker-100 placeholder:focus:text-transparent">
|
||||||
|
{value?.split("\n")[1]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverObject>
|
</PopoverObject>
|
||||||
@@ -148,10 +175,10 @@ const DashboardInputField = ({
|
|||||||
}
|
}
|
||||||
if (type === "value") {
|
if (type === "value") {
|
||||||
return (
|
return (
|
||||||
<div className="flex-col w-full">
|
<div className="w-full flex-col">
|
||||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full">
|
<div className="group relative flex w-full flex-col justify-center whitespace-pre">
|
||||||
{overrideEnabled === true && (
|
{overrideEnabled === true && (
|
||||||
<div className="bg-primary-500 rounded-sm absolute top-[0.1rem] right-[0.1rem] z-0 w-min text-xxs px-1 text-black opacity-80">
|
<div className="absolute top-[0.1rem] right-[0.1rem] z-0 w-min rounded-sm bg-primary-500 px-1 text-xxs text-black opacity-80">
|
||||||
Override enabled
|
Override enabled
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -160,20 +187,20 @@ const DashboardInputField = ({
|
|||||||
onChange={(e) => onChangeHandler(e.target.value, id)}
|
onChange={(e) => onChangeHandler(e.target.value, id)}
|
||||||
onScroll={syncScroll}
|
onScroll={syncScroll}
|
||||||
className={`${
|
className={`${
|
||||||
blurred
|
blurred ? "text-transparent focus:text-transparent active:text-transparent" : ""
|
||||||
? "text-transparent focus:text-transparent active:text-transparent"
|
} ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none duration-200 no-scrollbar`}
|
||||||
: ""
|
|
||||||
} z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`${
|
className={`${
|
||||||
blurred && !overrideEnabled
|
blurred && !overrideEnabled
|
||||||
? "text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200"
|
? "text-bunker-800 duration-200 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400"
|
||||||
: ""
|
: ""
|
||||||
} ${overrideEnabled ? "text-primary-300" : "text-gray-400"}
|
} ${overrideEnabled ? "text-primary-300" : "text-gray-400"}
|
||||||
absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? "invisible" : "visible"} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
absolute z-0 flex flex-row whitespace-pre font-mono ${
|
||||||
|
blurred ? "invisible" : "visible"
|
||||||
|
} ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar mt-0.5 h-10 w-full overflow-x-scroll bg-transparent px-2 py-2 text-sm outline-none duration-100 no-scrollbar peer-focus:visible`}
|
||||||
>
|
>
|
||||||
{value?.split(REGEX).map((word) => {
|
{value?.split(REGEX).map((word) => {
|
||||||
if (word.match(REGEX) !== null) {
|
if (word.match(REGEX) !== null) {
|
||||||
@@ -203,20 +230,24 @@ const DashboardInputField = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{blurred && (
|
{blurred && (
|
||||||
<div className={`absolute flex flex-row justify-between items-center z-0 peer pr-2 ${
|
<div
|
||||||
isSideBarOpen ? "bg-mineshaft-700 duration-200" : "bg-mineshaft-800"
|
className={`peer absolute z-0 flex flex-row items-center justify-between pr-2 ${
|
||||||
} peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip`}>
|
isSideBarOpen ? "bg-mineshaft-700 duration-200" : "bg-mineshaft-800"
|
||||||
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
} h-10 w-full text-clip text-bunker-400 duration-100 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden`}
|
||||||
|
>
|
||||||
|
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
||||||
{value?.split("").map(() => (
|
{value?.split("").map(() => (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
key={guidGenerator()}
|
key={guidGenerator()}
|
||||||
className="text-xxs mr-0.5"
|
className="mr-0.5 text-xxs"
|
||||||
icon={faCircle}
|
icon={faCircle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{value?.split("").length === 0 && <span className='text-bunker-400/80'>EMPTY</span>}
|
{value?.split("").length === 0 && <span className="text-bunker-400/80">EMPTY</span>}
|
||||||
|
</div>
|
||||||
|
<div className="invisible z-[100] cursor-default group-hover:visible">
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
</div>
|
</div>
|
||||||
<div className='invisible group-hover:visible cursor-default z-[100]'><FontAwesomeIcon icon={faEye} /></div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React from "react"
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -8,32 +8,35 @@ import Button from "../basic/buttons/Button";
|
|||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
isPlain?: boolean;
|
isPlain?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${
|
<div
|
||||||
!isPlain
|
className={`${
|
||||||
? "bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2"
|
!isPlain
|
||||||
: "cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center"}`}>
|
? "ml-2 h-[2.5rem] w-[4.5rem] rounded-md bg-[#9B3535] opacity-70 duration-200 hover:opacity-100"
|
||||||
{isPlain
|
: "justfy-center mr-2 flex h-[2.35rem] w-[1.5rem] cursor-pointer items-center"
|
||||||
? <div
|
}`}
|
||||||
onKeyDown={() => null}
|
>
|
||||||
role="button"
|
{isPlain ? (
|
||||||
tabIndex={0}
|
<div
|
||||||
onClick={onSubmit}
|
onKeyDown={() => null}
|
||||||
className="invisible group-hover:visible"
|
role="button"
|
||||||
>
|
tabIndex={0}
|
||||||
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5" icon={faXmark} />
|
onClick={onSubmit}
|
||||||
</div>
|
className="invisible group-hover:visible"
|
||||||
: <Button
|
>
|
||||||
text={String(t("Delete"))}
|
<FontAwesomeIcon
|
||||||
color="red"
|
className="mt-0.5 pl-2 pr-6 text-lg text-bunker-300 hover:text-red"
|
||||||
size="md"
|
icon={faXmark}
|
||||||
onButtonPressed={onSubmit}
|
/>
|
||||||
/>}
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button text={String(t("Delete"))} color="red" size="md" onButtonPressed={onSubmit} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@@ -18,7 +18,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
|
|||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
as="div"
|
as="div"
|
||||||
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||||
>
|
>
|
||||||
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
|
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
@@ -31,7 +31,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute z-[90] drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
<Menu.Items className="absolute right-0 z-[90] mt-0.5 w-[12rem] origin-top-right space-y-2 rounded-md border border-mineshaft-500 bg-bunker p-2 shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-xl focus:outline-none">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Button
|
<Button
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
|
@@ -8,7 +8,7 @@ import { parseDocument, Scalar, YAMLMap } from "yaml";
|
|||||||
|
|
||||||
import Button from "../basic/buttons/Button";
|
import Button from "../basic/buttons/Button";
|
||||||
import Error from "../basic/Error";
|
import Error from "../basic/Error";
|
||||||
import { useNotificationContext } from "../context/Notifications/NotificationProvider";
|
import { createNotification } from "../notifications";
|
||||||
import { parseDotEnv } from "../utilities/parseDotEnv";
|
import { parseDotEnv } from "../utilities/parseDotEnv";
|
||||||
import guidGenerator from "../utilities/randomId";
|
import guidGenerator from "../utilities/randomId";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const DropZone = ({
|
|||||||
numCurrentRows
|
numCurrentRows
|
||||||
}: DropZoneProps) => {
|
}: DropZoneProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
|
|
||||||
const handleDragEnter = (e: DragEvent) => {
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -133,7 +133,6 @@ const DropZone = ({
|
|||||||
createNotification({
|
createNotification({
|
||||||
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
|
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
|
||||||
type: "error",
|
type: "error",
|
||||||
timeoutMs: 10000
|
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
|
36
frontend/src/components/features/TtlFormLabel.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { FormLabel, Tooltip } from "../v2";
|
||||||
|
|
||||||
|
// To give users example of possible values of TTL
|
||||||
|
export const TtlFormLabel = ({ label }: { label: string }) => (
|
||||||
|
<div>
|
||||||
|
<FormLabel
|
||||||
|
label={label}
|
||||||
|
icon={
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<span>
|
||||||
|
1m, 2h, 3d.{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-700"
|
||||||
|
>
|
||||||
|
More
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faQuestionCircle}
|
||||||
|
size="sm"
|
||||||
|
className="relative bottom-1 right-1"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
1
frontend/src/components/features/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { TtlFormLabel } from "./TtlFormLabel";
|