Compare commits

..

192 Commits

Author SHA1 Message Date
0fdf5032f9 allow viewer to create and list dynamic secret 2024-03-26 17:50:06 -04:00
e258b84796 move to weekend 2024-03-26 15:37:24 -04:00
1ab6b21b25 add maintenance notice for april 2024-03-26 15:22:55 -04:00
775037539e update docs to include HA for infisical cloud 2024-03-26 14:48:41 -04:00
7c623562e1 Merge pull request #1627 from akhilmhdh/feat/dyn-sec-overview
feat: added dynamic secret to overview page
2024-03-26 14:14:02 -04:00
aef8d79101 feat: added dynamic secret to overview page 2024-03-26 23:40:41 +05:30
d735ec71b8 Merge pull request #1626 from jacobwgillespie/optimize-docker-builds
chore(ci): optimize Depot build workflows
2024-03-26 12:36:15 -04:00
84651d473b chore(ci): optimize Depot build workflows 2024-03-26 16:22:07 +00:00
9501386882 Merge pull request #1625 from Infisical/daniel/missing-changes
Fix: Minor naming changes
2024-03-26 17:11:03 +01:00
d11f958443 Requested changes 2024-03-26 17:07:25 +01:00
087a4bb7d2 Merge pull request #1624 from akhilmhdh/feat/ui-notification-errors
New toast notification for ui
2024-03-26 11:41:10 -04:00
750210e6c3 update doc item title 2024-03-26 11:22:11 -04:00
90cf4e9137 update license docs 2024-03-26 11:21:40 -04:00
17bb2e8a7d set dynamic secret to true 2024-03-26 10:26:22 -04:00
b912cd585c feat(ui): resolved dynamic secret merge conflict 2024-03-26 19:45:52 +05:30
282434de8e feat(ui): changed to a better toast for ui and a global error handler for all server error messages 2024-03-26 19:37:13 +05:30
1f939a5e58 Merge pull request #1611 from akhilmhdh/feat/pg-dynamic-secret
Dynamic secret #Postgres
2024-03-26 09:57:31 -04:00
ac0f5369de inject license server and fix wording 2024-03-26 09:53:43 -04:00
6eba64c975 fix merge 2024-03-26 09:41:32 -04:00
12515c1866 Merge branch 'main' into feat/pg-dynamic-secret 2024-03-26 09:30:30 -04:00
c882da2e1a feat: added license check for dynamic secret 2024-03-26 14:35:19 +05:30
8a7774f9ac feat(ui): updated api changes made 2024-03-26 14:10:25 +05:30
a7d2ec80c6 feat(server): updated dynamic secret names from feedback, added describe and fixed login not working 2024-03-26 13:18:31 +05:30
494543ec53 Delete .github/workflows/build-staging-and-deploy.yml 2024-03-26 00:01:39 -04:00
b7b875b6a7 add prod creds to pipeline 2024-03-25 21:23:53 -04:00
3ddd06a3d1 Revert "Update build-staging-and-deploy-aws.yml"
This reverts commit a1a8364cd19f6e441d0e934bc9478d5f38755aca.
2024-03-25 21:16:33 -04:00
a1a8364cd1 Update build-staging-and-deploy-aws.yml 2024-03-25 21:12:50 -04:00
3e51fcb546 Merge pull request #1623 from Infisical/daniel/secret-tags-docs
Feat: Standalone tag attaching & detaching
2024-03-26 01:14:00 +01:00
c52a16cc47 Update constants.ts 2024-03-25 21:56:53 +01:00
f91c77baa3 Docs: Attach / Detach tags 2024-03-25 21:53:39 +01:00
e7c2f6f88c Docs: Expose tags endpoints 2024-03-25 21:48:54 +01:00
f7c2d38aef Feat: (Standalone) Attach / Detach tags from secrets 2024-03-25 21:48:14 +01:00
cfb497dd58 Feat: Get secret tags by secret ID 2024-03-25 21:47:23 +01:00
f7122c21fd Fix: Allow query function to be called with undefined orgId, and handle it as an error 2024-03-25 21:43:08 +01:00
b23deca8e4 Feat: Attach/Detach tags to secret 2024-03-25 21:42:32 +01:00
b606990dfb Update secret-tag-router.ts 2024-03-25 21:41:12 +01:00
2240277243 Feat: Standalone tags documentation 2024-03-25 21:40:45 +01:00
c8c5caba62 Update Chart.yaml 2024-03-25 13:48:17 -04:00
f408a6f60c Update values.yaml 2024-03-25 13:48:01 -04:00
391ed0ed74 Update build-staging-and-deploy-aws.yml 2024-03-25 11:15:39 -04:00
aef40212d2 Merge pull request #1528 from rhythmbhiwani/cli-fix-update-vars
Fixed CLI issue of updating variables using `infisical secrets set`
2024-03-25 15:30:47 +01:00
5aa7cd46c1 Merge pull request #1594 from Salman2301/feat-cloudflare-secret-path
feat: add support for secret path for cloudflare page
2024-03-25 11:37:00 +05:30
6c0b916ad8 set version to short commit sha 2024-03-25 01:54:53 -04:00
d7bc80308d Merge pull request #1566 from Salman2301/fix-typo-input
fix class name typo
2024-03-25 11:14:55 +05:30
b7c7b242e8 Merge pull request #1578 from Salman2301/fix-select-key-nav
fix: add highlighted style for select component
2024-03-25 11:13:48 +05:30
b592f4cb6d update ui 2024-03-24 22:00:30 -07:00
cd0e1a87cf feat(server): resolved lint issue 2024-03-25 01:06:43 +05:30
b5d7699b8d feat(ui): made changes from feedback and force delete feature 2024-03-25 00:50:26 +05:30
69297bc16e feat(server): added limit to leases creation and support for force delete when external system fails to comply 2024-03-25 00:49:48 +05:30
37827367ed Merge pull request #1622 from Infisical/daniel/mi-project-creation-bug
Fix: Creating projects with Machine Identities that aren't org admins
2024-03-24 14:46:23 -04:00
403b1ce993 Merge pull request #1620 from Infisical/daniel/e2ee-button
Feat: Deprecate E2EE mode switching
2024-03-24 14:33:44 -04:00
c3c0006a25 Update project-service.ts 2024-03-24 17:15:10 +01:00
2241908d0a fix lining for gha 2024-03-24 00:52:21 -04:00
59b822510c update job names gha 2024-03-24 00:50:40 -04:00
d1408aff35 update pipeline 2024-03-24 00:49:47 -04:00
c67084f08d combine migration job with deploy 2024-03-24 00:47:46 -04:00
a280e002ed add prod deploy 2024-03-24 00:35:10 -04:00
76c4a8660f Merge pull request #1621 from redcubie/patch-1
Move DB_CONNECTION_URI to make sure DB credentials are initialized
2024-03-23 12:59:27 -04:00
8c54dd611e Move DB_CONNECTION_URI to make sure DB credentials are initialized 2024-03-23 18:49:15 +02:00
98ea2c1828 feat(agent): removed 5s polling algorithm to a simple one 2024-03-23 21:10:42 +05:30
5c75f526e7 Update build-staging-and-deploy-aws.yml 2024-03-22 17:18:45 -04:00
113e777b25 add wait for ecs 2024-03-22 16:16:22 -04:00
2a93449ffe add needs[] for gamm deploy gha 2024-03-22 14:56:38 -04:00
1ef1c042da add back other build steps 2024-03-22 14:55:55 -04:00
b64672a921 Update E2EESection.tsx 2024-03-22 19:33:53 +01:00
227e013502 Feat: Deprecate E2EE mode switching 2024-03-22 19:31:41 +01:00
88f7e4255e feat(agent): added dynamic secret change based re-trigger 2024-03-22 23:59:08 +05:30
44ca8c315e remove all stages except deploy in gha 2024-03-22 14:12:13 -04:00
7766a7f4dd Merge pull request #1619 from Infisical/daniel/mi-ux-fix
Update IdentityModal.tsx
2024-03-22 18:56:07 +01:00
3cb150a749 Update IdentityModal.tsx 2024-03-22 18:27:57 +01:00
9e9ce261c8 give gha permission to update git token 2024-03-22 12:59:51 -04:00
fab7167850 update oidc audience 2024-03-22 12:54:27 -04:00
c7de9aab4e Merge pull request #1618 from Infisical/gha-aws-pipeline
deploy to ecs using OIDC with aws
2024-03-22 22:13:09 +05:30
3560346f85 update step name 2024-03-22 12:42:32 -04:00
f0bf2f8dd0 seperate aws rds uri 2024-03-22 12:38:10 -04:00
2a6216b8fc deploy to ecs using OIDC with aws 2024-03-22 12:29:07 -04:00
a07d055347 feat(agent): added agent template support to pull dynamic secret 2024-03-22 19:29:24 +05:30
c05230f667 Merge pull request #1616 from Infisical/wait-for-job-helm
Update Chart.yaml
2024-03-22 19:03:32 +05:30
d68055a264 Update Chart.yaml
Update to multi arch and rootless
2024-03-22 09:28:44 -04:00
e3e62430ba feat: changed the whole api from projectid to slug 2024-03-22 14:18:12 +05:30
dc6056b564 Merge pull request #1614 from francodalmau/fix-environment-popups-cancel-action
Fix add and update environment popups cancel button
2024-03-21 21:28:28 -04:00
94f0811661 Fix add and update environment popups cancel button 2024-03-21 20:09:57 -03:00
7b84ae6173 Update Chart.yaml 2024-03-21 15:08:38 -04:00
5710a304f8 Merge pull request #1533 from Infisical/daniel/k8-operator-machine-identities
Feat: K8 Operator Machine Identity Support
2024-03-21 15:01:50 -04:00
91e3bbba34 Fix: Requested changes 2024-03-21 19:58:10 +01:00
02112ede07 Fix: Requested changes 2024-03-21 19:53:21 +01:00
08cfbf64e4 Fix: Error handing 2024-03-21 19:37:12 +01:00
18da522b45 Chore: Helm charts 2024-03-21 18:35:00 +01:00
8cf68fbd9c Generated 2024-03-21 17:12:42 +01:00
d6b82dfaa4 Fix: Rebase sample conflicts 2024-03-21 17:09:41 +01:00
7bd4eed328 Chore: Generate K8 helm charts 2024-03-21 17:08:01 +01:00
0341c32da0 Fix: Change credentials -> credentialsRef 2024-03-21 17:08:01 +01:00
caea055281 Feat: Improve K8 docs 2024-03-21 17:08:01 +01:00
c08c78de8d Feat: Rename universalAuthMachineIdentity to universalAuth 2024-03-21 17:08:01 +01:00
3765a14246 Fix: Generate new types 2024-03-21 17:08:01 +01:00
c5a11e839b Feat: Deprecate Service Accounts 2024-03-21 17:08:01 +01:00
93bd3d8270 Docs: Simplified docs more 2024-03-21 17:07:56 +01:00
b9601dd418 Update kubernetes.mdx 2024-03-21 17:07:56 +01:00
ae3bc04b07 Docs 2024-03-21 17:07:31 +01:00
11edefa66f Feat: Added project slug support 2024-03-21 17:07:31 +01:00
f71459ede0 Slugs 2024-03-21 17:07:31 +01:00
33324a5a3c Type generation 2024-03-21 17:07:31 +01:00
5c6781a705 Update machine-identity-token.go 2024-03-21 17:07:31 +01:00
71e31518d7 Feat: Add machine identity token handler 2024-03-21 17:07:31 +01:00
f6f6db2898 Fix: Moved update attributes type to models 2024-03-21 17:07:31 +01:00
55780b65d3 Feat: Machine Identity support (token refreshing logic) 2024-03-21 17:07:31 +01:00
83bbf9599d Feat: Machine Identity support 2024-03-21 17:07:31 +01:00
f8f2b2574d Feat: Machine Identity support (types) 2024-03-21 17:07:31 +01:00
318d12addd Feat: Machine Identity support 2024-03-21 17:07:31 +01:00
872a28d02a Feat: Machine Identity support for K8 2024-03-21 17:06:49 +01:00
6f53a5631c Fix: Double prints 2024-03-21 17:06:49 +01:00
ff2098408d Update sample.yaml 2024-03-21 17:06:48 +01:00
9e85d9bbf0 Example 2024-03-21 17:05:46 +01:00
0f3a48bb32 Generated 2024-03-21 17:05:46 +01:00
f869def8ea Added new types 2024-03-21 17:05:46 +01:00
378bc57a88 Merge pull request #1480 from akhilmhdh/docs/rotation-doc-update
docs: improved secret rotation documentation with better understanding
2024-03-21 11:17:19 -04:00
242179598b fix types, rephrase, and revise rotation docs 2024-03-21 11:03:41 -04:00
70fe80414d feat(server): added some more validation and feedback for deletion etc 2024-03-21 20:23:44 +05:30
e201e80a06 feat(ui): completed ui for dynamic secret 2024-03-21 20:23:44 +05:30
177cd385cc feat(ui): added dynamic secret and lease api hook 2024-03-21 20:22:30 +05:30
ab48c3b4fe feat(server): completed integration of dynamic server routes 2024-03-21 20:22:30 +05:30
69f36d1df6 feat(server): service for dynamic secret lease and queue service for revocation 2024-03-21 20:22:30 +05:30
11c7b5c674 feat(server): service for dynamic secret config 2024-03-21 20:22:30 +05:30
ee29577e6d feat(server): added dynamic secret database schema 2024-03-21 20:22:30 +05:30
e3e049b66c Update build-staging-and-deploy.yml 2024-03-20 22:14:46 -04:00
878e4a79e7 Merge pull request #1606 from Infisical/daniel/ui-imported-folders-fix
Fix: UI indicator for imports
2024-03-20 22:08:50 -04:00
609ce8e5cc Fix: Improved UI import indicators 2024-03-21 03:06:14 +01:00
04c1ea9b11 Update build-staging-and-deploy.yml 2024-03-20 18:03:49 -04:00
3baca73e53 add seperate step for ecr build 2024-03-20 16:25:54 -04:00
36adf6863b Fix: UI secret import indicator 2024-03-20 21:09:54 +01:00
6363e7d30a Update index.ts 2024-03-20 20:26:28 +01:00
f9621fad8e Fix: Remove duplicate type 2024-03-20 20:26:00 +01:00
90be28b87a Feat: Import indicator 2024-03-20 20:25:48 +01:00
671adee4d7 Feat: Indicator for wether or not secrets are imported 2024-03-20 20:24:06 +01:00
c9cb90c98e Feat: Add center property to tooltip 2024-03-20 20:23:21 +01:00
9f691df395 Merge pull request #1607 from Infisical/fix-secrets-by-name
set imports=true for get secret by name
2024-03-20 23:50:15 +05:30
d702a61586 set imports=true for get secret by name 2024-03-20 14:06:16 -04:00
1c16f406a7 remove debug 2024-03-20 13:06:29 -04:00
90f739caa6 correct repo name env 2024-03-20 13:05:59 -04:00
ede8b6f286 add .env context for ecr tag 2024-03-20 13:00:06 -04:00
232c547d75 correct ecr image tag 2024-03-20 11:54:33 -04:00
fe08bbb691 push to ecr 2024-03-20 11:46:47 -04:00
2bd06ecde4 login into ecr 2024-03-20 11:31:39 -04:00
08b79d65ea Fix: Remove unused lint disable 2024-03-20 14:55:02 +01:00
4e1733ba6c Fix: More reverting 2024-03-20 14:44:40 +01:00
a4e495ea1c Fix: Restructured frontend 2024-03-20 14:42:34 +01:00
a750d68363 Fix: Reverted backend changes 2024-03-20 14:40:24 +01:00
d7161a353d Fix: Better variable naming 2024-03-20 13:15:51 +01:00
12c414817f Fix: Remove debugging logs 2024-03-20 13:14:18 +01:00
e5e494d0ee Fix: Also display imported folder indicator for nested folders 2024-03-20 13:13:50 +01:00
5a21b85e9e Fix: Removed overlap from other working branch 2024-03-20 13:13:19 +01:00
348fdf6429 Feat: Visualize imported folders in overview page (include imported folders in response) 2024-03-20 12:56:11 +01:00
88e609cb66 Feat: New types for imported folders 2024-03-20 12:55:40 +01:00
78058d691a Enhancement: Add disabled prop to Tooltip component 2024-03-20 12:55:08 +01:00
1d465a50c3 Feat: Visualize imported folders in overview page 2024-03-20 12:54:44 +01:00
ffc7249c7c update diagram 2024-03-19 23:44:12 -04:00
90bcf23097 Update README.md 2024-03-19 23:36:07 -04:00
5fa4d9029d Merge pull request #1577 from Salman2301/fix-notification-z-index
fix: notification error behind detail sidebar
2024-03-19 18:56:14 -04:00
7160cf58ee Merge branch 'main' into fix-notification-z-index 2024-03-19 18:50:58 -04:00
6b2d757e39 remove outdated healthcheck 2024-03-19 17:21:46 -04:00
c075fcceca Merge pull request #1591 from Infisical/daniel/prettier-fix
Chore: Prettier formatting
2024-03-19 21:23:11 +01:00
e25f5dd65f Merge pull request #1605 from Infisical/creation-policy-k8s
add managed secret creation policy
2024-03-19 15:23:16 -04:00
3eef023c30 add managed secret creation policy 2024-03-19 14:58:17 -04:00
e63deb0860 Patch org role slug validation 2024-03-19 10:23:00 -07:00
02b2851990 Merge pull request #1601 from Infisical/fix/db-host
fix(server): updated secret rotation to pick on db host in validation
2024-03-19 10:03:12 -04:00
cb828200e1 fix(server): updated secret rotation to pick on db host in validation 2024-03-19 13:56:21 +00:00
77d068ae2c Merge pull request #1599 from Infisical/daniel/improve-create-project
Fix: Remove required org slug from create project route
2024-03-19 18:31:16 +05:30
8702af671d Fix: Typings error 2024-03-19 13:45:57 +01:00
31c0fd96ea Update UserInfoStep.tsx 2024-03-19 13:39:34 +01:00
2c539697df Feat: Remove orgSlug from create project endpoint 2024-03-19 13:39:24 +01:00
ae97b74933 Feat: Improve create project, remove organization slug from frontend 2024-03-19 13:38:58 +01:00
3e6af2dae5 Merge pull request #1597 from Infisical/daniel/api-endpoint-fix
Fix: Mintlify interactive API docs defaulting to localhost server
2024-03-19 16:32:49 +05:30
3c91e1127f Fix: Mintlify docs defaulting to localhost endpoint 2024-03-19 11:58:01 +01:00
0e31a9146a Update ee.mdx 2024-03-18 22:10:09 -07:00
d2a93eb1d2 feat: add support for secret path for cloudflare page 2024-03-18 21:31:21 +05:30
fa1b28b33f Update .eslintrc.js 2024-03-18 16:07:49 +01:00
415cf31b2d Fix: Lint bug (Cannot read properties of undefined (reading 'getTokens')) 2024-03-18 16:01:14 +01:00
9002e6cb33 Fix: Format entire frontend properly 2024-03-18 16:00:03 +01:00
1ede551c3e Fix: Format entire frontend properly 2024-03-18 15:59:47 +01:00
b7b43858f6 Fix: Format entire frontend properly 2024-03-18 15:55:01 +01:00
c91789e6d0 Merge pull request #1590 from Infisical/daniel/ts-comments
Fix: Github warnings / Lint warnings
2024-03-18 20:19:26 +05:30
f73c807aa0 Merge pull request #1589 from Infisical/daniel/ui-improvements
Fix: Select organization UX & project card enhancements
2024-03-18 20:14:39 +05:30
d1dacd81aa Chore: UX improvements to project cards 2024-03-18 15:36:59 +01:00
e8b635ce37 Fix: Code cleanup & truncate organization names 2024-03-18 15:36:01 +01:00
203e00216f fix: add highlighted style for select component 2024-03-16 17:53:48 +05:30
ee215bccfa fix: notification error behind detail sidebar 2024-03-16 08:34:21 +05:30
7a3a6663f1 fix class name typo 2024-03-14 03:37:54 +05:30
8c491668dc docs: updated images of inputs in secret rotation 2024-03-07 23:17:32 +05:30
c873e2cba8 docs: updated secret rotation doc with images 2024-03-05 15:42:53 +05:30
1bc045a7fa update overview sendgrid 2024-03-05 15:42:53 +05:30
533de93199 docs: improved secret rotation documentation with better understanding 2024-03-05 15:42:53 +05:30
115b4664bf Fixed CLI issue of updating variables using infisical secrets set 2024-03-05 03:43:06 +05:30
486 changed files with 10651 additions and 4771 deletions

View File

@ -3,9 +3,6 @@
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# Required
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# JWT
# Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
@ -16,6 +13,9 @@ POSTGRES_PASSWORD=infisical
POSTGRES_USER=infisical
POSTGRES_DB=infisical
# Required
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# Redis
REDIS_URL=redis://redis:6379

View File

@ -41,6 +41,7 @@ jobs:
load: true
context: backend
tags: infisical/infisical:test
platforms: linux/amd64,linux/arm64
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@ -92,6 +93,7 @@ jobs:
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

View 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

View File

@ -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

View File

@ -118,9 +118,6 @@ WORKDIR /backend
ENV TELEMETRY_ENABLED true
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080
EXPOSE 443

View File

@ -10,7 +10,8 @@
<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/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>
<p align="center">

View File

@ -21,6 +21,8 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { 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 { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@ -62,7 +64,7 @@ declare module "fastify" {
authMethod: ActorAuthMethod;
type: ActorType;
id: string;
orgId?: string;
orgId: string;
};
// passport data
passportUser: {
@ -117,6 +119,8 @@ declare module "fastify" {
trustedIp: TTrustedIpServiceFactory;
secretBlindIndex: TSecretBlindIndexServiceFactory;
telemetry: TTelemetryServiceFactory;
dynamicSecret: TDynamicSecretServiceFactory;
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -17,6 +17,12 @@ import {
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate,
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate,
TDynamicSecrets,
TDynamicSecretsInsert,
TDynamicSecretsUpdate,
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate,
@ -340,6 +346,12 @@ declare module "knex/types/tables" {
TSecretSnapshotFoldersInsert,
TSecretSnapshotFoldersUpdate
>;
[TableName.DynamicSecret]: Knex.CompositeTableType<TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate>;
[TableName.DynamicSecretLease]: Knex.CompositeTableType<
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate
>;
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;

View 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);
}

View 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>>;

View 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>>;

View File

@ -3,6 +3,8 @@ export * from "./audit-logs";
export * from "./auth-token-sessions";
export * from "./auth-tokens";
export * from "./backup-private-key";
export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
export * from "./git-app-install-sessions";
export * from "./git-app-org";
export * from "./identities";

View File

@ -59,6 +59,8 @@ export enum TableName {
GitAppOrg = "git_app_org",
SecretScanningGitRisk = "secret_scanning_git_risks",
TrustedIps = "trusted_ips",
DynamicSecret = "dynamic_secrets",
DynamicSecretLease = "dynamic_secret_leases",
// junction tables with tags
JnSecretTag = "secret_tag_junction",
SecretVersionTag = "secret_version_tag_junction"

View File

@ -19,7 +19,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
.min(1)
.trim()
.refine(
(val) => Object.keys(OrgMembershipRole).includes(val),
(val) => !Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {

View File

@ -146,7 +146,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
offset: req.query.startIndex,
limit: req.query.count,
filter: req.query.filter,
orgId: req.permission.orgId as string
orgId: req.permission.orgId
});
return users;
}
@ -184,7 +184,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const user = await req.server.services.scim.getScimUser({
userId: req.params.userId,
orgId: req.permission.orgId as string
orgId: req.permission.orgId
});
return user;
}
@ -243,7 +243,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
email: primaryEmail,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
orgId: req.permission.orgId as string
orgId: req.permission.orgId
});
return user;
@ -280,7 +280,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const user = await req.server.services.scim.updateScimUser({
userId: req.params.userId,
orgId: req.permission.orgId as string,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return user;
@ -330,7 +330,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
userId: req.params.userId,
orgId: req.permission.orgId as string,
orgId: req.permission.orgId,
active: req.body.active
});
return user;

View File

@ -15,6 +15,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
dynamicSecret: true,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,

View File

@ -27,6 +27,7 @@ export type TFeatureSet = {
tier: -1;
workspaceLimit: null;
workspacesUsed: 0;
dynamicSecret: true;
memberLimit: null;
membersUsed: 0;
environmentLimit: null;

View File

@ -9,6 +9,7 @@ import jmespath from "jmespath";
import knex from "knex";
import { getConfig } from "@app/lib/config/env";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
@ -89,7 +90,7 @@ export const secretRotationDbFn = async ({
const appCfg = getConfig();
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host))
if (host === "localhost" || host === "127.0.0.1" || getDbConnectionHost(appCfg.DB_CONNECTION_URI) === host)
throw new Error("Invalid db host");
const db = knex({

View File

@ -194,6 +194,25 @@ export const FOLDERS = {
}
} as const;
export const SECRETS = {
ATTACH_TAGS: {
secretName: "The name of the secret to attach tags to.",
secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
tagSlugs: "An array of existing tag slugs to attach to the secret."
},
DETACH_TAGS: {
secretName: "The name of the secret to detach tags from.",
secretPath: "The path of the secret to detach tags from.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
tagSlugs: "An array of existing tag slugs to detach from the secret."
}
} as const;
export const RAW_SECRETS = {
LIST: {
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."
}
} 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;

View File

@ -18,6 +18,7 @@ const envSchema = z
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
),
MAX_LEASE_LIMIT: z.coerce.number().default(10000),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"),

View File

@ -59,6 +59,18 @@ export class BadRequestError extends Error {
}
}
export class DisableRotationErrors extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message: string; name?: string; error?: unknown }) {
super(message);
this.name = name || "DisableRotationErrors";
this.error = error;
}
}
export class ScimRequestError extends Error {
name: string;

View File

@ -0,0 +1,11 @@
import { URL } from "url"; // Import the URL class
export const getDbConnectionHost = (urlString: string) => {
try {
const url = new URL(urlString);
// Split hostname and port (if provided)
return url.hostname.split(":")[0];
} catch (error) {
return null;
}
};

View File

@ -4,6 +4,7 @@ import { Tables } from "knex/types/tables";
import { DatabaseError } from "../errors";
export * from "./connection";
export * from "./join";
export * from "./select";

View File

@ -13,7 +13,7 @@ export type TProjectPermission = {
actorId: string;
projectId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
actorOrgId: string;
};
export type RequiredKeys<T> = {

View File

@ -18,7 +18,8 @@ export enum QueueName {
SecretWebhook = "secret-webhook",
SecretFullRepoScan = "secret-full-repo-scan",
SecretPushEventScan = "secret-push-event-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost"
UpgradeProjectToGhost = "upgrade-project-to-ghost",
DynamicSecretRevocation = "dynamic-secret-revocation"
}
export enum QueueJobs {
@ -30,7 +31,9 @@ export enum QueueJobs {
TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull",
SecretScan = "secret-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost-job"
UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
DynamicSecretRevocation = "dynamic-secret-revocation",
DynamicSecretPruning = "dynamic-secret-pruning"
}
export type TQueueJobTypes = {
@ -86,6 +89,19 @@ export type TQueueJobTypes = {
name: QueueJobs.TelemetryInstanceStats;
payload: undefined;
};
[QueueName.DynamicSecretRevocation]:
| {
name: QueueJobs.DynamicSecretRevocation;
payload: {
leaseId: string;
};
}
| {
name: QueueJobs.DynamicSecretPruning;
payload: {
dynamicSecretCfgId: string;
};
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@ -16,7 +16,7 @@ export type TAuthMode =
userId: string;
tokenVersionId: string; // the session id of token used
user: TUsers;
orgId?: string;
orgId: string;
authMethod: AuthMethod;
}
| {
@ -119,7 +119,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
userId: user.id,
tokenVersionId,
actor,
orgId,
orgId: orgId as string,
authMethod: token.authMethod
};
break;

View File

@ -14,13 +14,13 @@ export const fastifySwagger = fp(async (fastify) => {
version: "0.0.1"
},
servers: [
{
url: "http://localhost:8080",
description: "Local server"
},
{
url: "https://app.infisical.com",
description: "Production server"
},
{
url: "http://localhost:8080",
description: "Local server"
}
],
components: {

View File

@ -47,6 +47,12 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { 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 { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { identityServiceFactory } from "@app/services/identity/identity-service";
@ -196,6 +202,8 @@ export const registerRoutes = async (
const gitAppOrgDAL = gitAppDALFactory(db);
const secretScanningDAL = secretScanningDALFactory(db);
const licenseDAL = licenseDALFactory(db);
const dynamicSecretDAL = dynamicSecretDALFactory(db);
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@ -550,6 +558,34 @@ export const registerRoutes = async (
licenseService
});
const dynamicSecretProviders = buildDynamicSecretProviders();
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
queueService,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
dynamicSecretDAL
});
const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL,
dynamicSecretQueueService,
dynamicSecretDAL,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
folderDAL,
permissionService,
licenseService
});
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL,
permissionService,
dynamicSecretQueueService,
dynamicSecretDAL,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
folderDAL,
licenseService
});
await superAdminService.initServerCfg();
//
// setup the communication with license key server
@ -591,6 +627,8 @@ export const registerRoutes = async (
secretApprovalPolicy: sapService,
secretApprovalRequest: sarService,
secretRotation: secretRotationService,
dynamicSecret: dynamicSecretService,
dynamicSecretLease: dynamicSecretLeaseService,
snapshot: snapshotService,
saml: samlService,
ldap: ldapService,

View File

@ -1,6 +1,11 @@
import { z } from "zod";
import { IntegrationAuthsSchema, SecretApprovalPoliciesSchema, UsersSchema } from "@app/db/schemas";
import {
DynamicSecretsSchema,
IntegrationAuthsSchema,
SecretApprovalPoliciesSchema,
UsersSchema
} from "@app/db/schemas";
// sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod
@ -56,3 +61,11 @@ export const secretRawSchema = z.object({
secretValue: z.string(),
secretComment: z.string().optional()
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
});

View 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 };
}
});
};

View 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 };
}
});
};

View File

@ -1,6 +1,8 @@
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-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 { registerIdentityRouter } from "./identity-router";
import { registerIdentityUaRouter } from "./identity-ua";
@ -52,6 +54,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
{ 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(registerIntegrationRouter, { prefix: "/integration" });
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });

View File

@ -1,6 +1,7 @@
import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas";
import { SECRET_TAGS } from "@app/lib/api-docs";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -10,7 +11,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "GET",
schema: {
params: z.object({
projectId: z.string().trim()
projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
}),
response: {
200: z.object({
@ -36,12 +37,12 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "POST",
schema: {
params: z.object({
projectId: z.string().trim()
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}),
body: z.object({
name: z.string().trim(),
slug: z.string().trim(),
color: z.string()
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}),
response: {
200: z.object({
@ -68,8 +69,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "DELETE",
schema: {
params: z.object({
projectId: z.string().trim(),
tagId: z.string().trim()
projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
}),
response: {
200: z.object({

View File

@ -150,8 +150,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECTS.CREATE.slug),
organizationSlug: z.string().trim().describe(PROJECTS.CREATE.organizationSlug)
.describe(PROJECTS.CREATE.slug)
}),
response: {
200: z.object({
@ -166,7 +165,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
orgSlug: req.body.organizationSlug,
workspaceName: req.body.projectName,
slug: req.body.slug
});

View File

@ -10,7 +10,7 @@ import {
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { RAW_SECRETS } from "@app/lib/api-docs";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@ -23,6 +23,124 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
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({
url: "/raw",
method: "GET",

View File

@ -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 };
};

View File

@ -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
};
};

View File

@ -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
};
};

View File

@ -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">;

View 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;
};

View 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
};
};

View 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">;

View File

@ -0,0 +1,6 @@
import { DynamicSecretProviders } from "./models";
import { SqlDatabaseProvider } from "./sql-database";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
});

View 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 }>;
};

View 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
};
};

View File

@ -328,7 +328,7 @@ export const projectMembershipServiceFactory = ({
);
const hasCustomRole = Boolean(customInputRoles.length);
if (hasCustomRole) {
const plan = await licenseService.getPlan(actorOrgId as string);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.rbac)
throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."

View File

@ -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 {
if (!orgId) {
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
}
const projects = await db(TableName.ProjectMembership)
.where(`${TableName.Project}.slug`, slug)
.where(`${TableName.Project}.orgId`, orgId)

View File

@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@ -92,7 +92,6 @@ export const projectServiceFactory = ({
* Create workspace. Make user the admin
* */
const createProject = async ({
orgSlug,
actor,
actorId,
actorOrgId,
@ -100,13 +99,7 @@ export const projectServiceFactory = ({
workspaceName,
slug: projectSlug
}: TCreateProjectDTO) => {
if (!orgSlug) {
throw new BadRequestError({
message: "Must provide organization slug to create project"
});
}
const organization = await orgDAL.findOne({ slug: orgSlug });
const organization = await orgDAL.findOne({ id: actorOrgId });
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
actor,
@ -291,10 +284,11 @@ export const projectServiceFactory = ({
// Get the role permission for the identity
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
ProjectMembershipRole.Admin,
OrgMembershipRole.Member,
organization.id
);
// Identity has to be at least a member in order to create projects
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege)
throw new ForbiddenRequestError({

View File

@ -24,7 +24,6 @@ export type TCreateProjectDTO = {
actorAuthMethod: ActorAuthMethod;
actorId: string;
actorOrgId?: string;
orgSlug: string;
workspaceName: string;
slug?: string;
};

View File

@ -232,6 +232,7 @@ export const secretFolderServiceFactory = ({
if (!parentFolder) return [];
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
return folders;
};

View File

@ -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 (
folderId: string,
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
@ -184,6 +205,7 @@ export const secretDALFactory = (db: TDbClient) => {
bulkUpdate,
deleteMany,
bulkUpdateNoVersionIncrement,
getSecretTags,
findByFolderId,
findByBlindIndexes
};

View File

@ -22,6 +22,7 @@ import { TSecretDALFactory } from "./secret-dal";
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue";
import {
TAttachSecretTagsDTO,
TCreateBulkSecretDTO,
TCreateSecretDTO,
TCreateSecretRawDTO,
@ -47,7 +48,7 @@ type TSecretServiceFactoryDep = {
secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@ -307,6 +308,7 @@ export const secretServiceFactory = ({
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const { secretName, ...el } = inputSecret;
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
@ -442,6 +444,7 @@ export const secretServiceFactory = ({
const folderId = folder.id;
const secrets = await secretDAL.findByFolderId(folderId, actorId);
if (includeImports) {
const secretImports = await secretImportDAL.find({ folderId });
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
@ -994,7 +997,209 @@ export const secretServiceFactory = ({
return secretVersions;
};
const attachTags = async ({
secretName,
tagSlugs,
path: secretPath,
environment,
type,
projectSlug,
actor,
actorAuthMethod,
actorOrgId,
actorId
}: TAttachSecretTagsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
await projectDAL.checkProjectUpgradeStatus(project.id);
const secret = await getSecretByName({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId: project.id,
environment,
path: secretPath,
secretName,
type
});
if (!secret) {
throw new BadRequestError({ message: "Secret not found" });
}
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
const tags = await secretTagDAL.find({
projectId: project.id,
$in: {
slug: tagSlugs
}
});
if (tags.length !== tagSlugs.length) {
throw new BadRequestError({ message: "One or more tags not found." });
}
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) {
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
}
const combinedTags = new Set([...existingSecretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId: folder.id,
projectId: project.id,
inputSecrets: [
{
filter: { id: secret.id },
data: {
tags: Array.from(combinedTags)
}
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
return {
...updatedSecret[0],
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
};
};
const detachTags = async ({
secretName,
tagSlugs,
path: secretPath,
environment,
type,
projectSlug,
actor,
actorAuthMethod,
actorOrgId,
actorId
}: TAttachSecretTagsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
await projectDAL.checkProjectUpgradeStatus(project.id);
const secret = await getSecretByName({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId: project.id,
environment,
path: secretPath,
secretName,
type
});
if (!secret) {
throw new BadRequestError({ message: "Secret not found" });
}
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
const tags = await secretTagDAL.find({
projectId: project.id,
$in: {
slug: tagSlugs
}
});
if (tags.length !== tagSlugs.length) {
throw new BadRequestError({ message: "One or more tags not found." });
}
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
// Make sure all the tags exist on the secret
const tagIdsToRemove = tags.map((tag) => tag.id);
const secretTagIds = existingSecretTags.map((tag) => tag.id);
if (!tagIdsToRemove.every((el) => secretTagIds.includes(el))) {
throw new BadRequestError({ message: "One or more tags not found on the secret" });
}
const newTags = existingSecretTags.filter((tag) => !tagIdsToRemove.includes(tag.id));
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId: folder.id,
projectId: project.id,
inputSecrets: [
{
filter: { id: secret.id },
data: {
tags: newTags.map((tag) => tag.id)
}
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
return {
...updatedSecret[0],
tags: newTags
};
};
return {
attachTags,
detachTags,
createSecret,
deleteSecret,
updateSecret,

View File

@ -206,6 +206,15 @@ export type TFnSecretBulkUpdate = {
tx?: Knex;
};
export type TAttachSecretTagsDTO = {
projectSlug: string;
secretName: string;
tagSlugs: string[];
environment: string;
path: string;
type: SecretType;
} & Omit<TProjectPermission, "projectId">;
export type TFnSecretBulkDelete = {
folderId: string;
projectId: string;

1
cli/.gitignore vendored
View File

@ -1,2 +1,3 @@
.infisical.json
dist/
agent-config.test.yaml

View File

@ -406,14 +406,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
return nil
}
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, secretName))
if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
@ -535,3 +535,23 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
return getRawSecretsV3Response, nil
}
func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDynamicSecretLeaseV1Request) (CreateDynamicSecretLeaseV1Response, error) {
var createDynamicSecretLeaseResponse CreateDynamicSecretLeaseV1Response
response, err := httpClient.
R().
SetResult(&createDynamicSecretLeaseResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v1/dynamic-secrets/leases", config.INFISICAL_URL))
if err != nil {
return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unable to complete api request [err=%w]", err)
}
if response.IsError() {
return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return createDynamicSecretLeaseResponse, nil
}

View File

@ -401,7 +401,6 @@ type DeleteSecretV3Request struct {
}
type UpdateSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"`
Environment string `json:"environment"`
Type string `json:"type"`
@ -501,6 +500,28 @@ type UniversalAuthRefreshResponse struct {
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
}
type CreateDynamicSecretLeaseV1Request struct {
Environment string `json:"environment"`
ProjectSlug string `json:"projectSlug"`
SecretPath string `json:"secretPath,omitempty"`
Slug string `json:"slug"`
TTL string `json:"ttl,omitempty"`
}
type CreateDynamicSecretLeaseV1Response struct {
Lease struct {
Id string `json:"id"`
ExpireAt time.Time `json:"expireAt"`
} `json:"lease"`
DynamicSecret struct {
Id string `json:"id"`
DefaultTTL string `json:"defaultTTL"`
MaxTTL string `json:"maxTTL"`
Type string `json:"type"`
} `json:"dynamicSecret"`
Data map[string]interface{} `json:"data"`
}
type GetRawSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`

View File

@ -14,6 +14,7 @@ import (
"os/signal"
"path"
"runtime"
"slices"
"strings"
"sync"
"syscall"
@ -33,6 +34,9 @@ import (
const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"
// duration to reduce from expiry of dynamic leases so that it gets triggered before expiry
const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15
type Config struct {
Infisical InfisicalConfig `yaml:"infisical"`
Auth AuthConfig `yaml:"auth"`
@ -84,6 +88,115 @@ type Template struct {
} `yaml:"config"`
}
func newAgentTemplateChannels(templates []Template) map[string]chan bool {
// we keep each destination as an identifier for various channel
templateChannel := make(map[string]chan bool)
for _, template := range templates {
templateChannel[template.DestinationPath] = make(chan bool)
}
return templateChannel
}
type DynamicSecretLease struct {
LeaseID string
ExpireAt time.Time
Environment string
SecretPath string
Slug string
ProjectSlug string
Data map[string]interface{}
TemplateIDs []int
}
type DynamicSecretLeaseManager struct {
leases []DynamicSecretLease
mutex sync.Mutex
}
func (d *DynamicSecretLeaseManager) Prune() {
d.mutex.Lock()
defer d.mutex.Unlock()
d.leases = slices.DeleteFunc(d.leases, func(s DynamicSecretLease) bool {
return time.Now().After(s.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second))
})
}
func (d *DynamicSecretLeaseManager) Append(lease DynamicSecretLease) {
d.mutex.Lock()
defer d.mutex.Unlock()
index := slices.IndexFunc(d.leases, func(s DynamicSecretLease) bool {
if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug {
return true
}
return false
})
if index != -1 {
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, lease.TemplateIDs...)
return
}
d.leases = append(d.leases, lease)
}
func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, secretPath, slug string, templateId int) {
d.mutex.Lock()
defer d.mutex.Unlock()
index := slices.IndexFunc(d.leases, func(lease DynamicSecretLease) bool {
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
return true
}
return false
})
if index != -1 {
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, templateId)
}
}
func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease {
d.mutex.Lock()
defer d.mutex.Unlock()
for _, lease := range d.leases {
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
return &lease
}
}
return nil
}
// for a given template find the first expiring lease
// The bool indicates whether it contains valid expiry list
func (d *DynamicSecretLeaseManager) GetFirstExpiringLeaseTime(templateId int) (time.Time, bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
if len(d.leases) == 0 {
return time.Time{}, false
}
var firstExpiry time.Time
for i, el := range d.leases {
if i == 0 {
firstExpiry = el.ExpireAt
}
newLeaseTime := el.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second)
if newLeaseTime.Before(firstExpiry) {
firstExpiry = newLeaseTime
}
}
return firstExpiry, true
}
func NewDynamicSecretLeaseManager(sigChan chan os.Signal) *DynamicSecretLeaseManager {
manager := &DynamicSecretLeaseManager{}
return manager
}
func ReadFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
}
@ -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
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag)
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateId)
funcs := template.FuncMap{
"secret": secretFunction,
"secret": secretFunction,
"dynamic_secret": dynamicSecretFunction,
}
templateName := path.Base(templatePath)
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath)
if err != nil {
return nil, err
@ -256,7 +397,7 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string,
return &buf, nil
}
func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) {
func ProcessBase64Template(templateId int, encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
// custom template function to fetch secrets from Infisical
decoded, err := base64.StdEncoding.DecodeString(encodedTemplate)
if err != nil {
@ -266,8 +407,10 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
templateString := string(decoded)
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
funcs := template.FuncMap{
"secret": secretFunction,
"secret": secretFunction,
"dynamic_secret": dynamicSecretFunction,
}
templateName := "base64Template"
@ -285,7 +428,7 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken
return &buf, nil
}
type TokenManager struct {
type AgentManager struct {
accessToken string
accessTokenTTL time.Duration
accessTokenMaxTTL time.Duration
@ -294,6 +437,7 @@ type TokenManager struct {
mutex sync.Mutex
filePaths []Sink // Store file paths if needed
templates []Template
dynamicSecretLeases *DynamicSecretLeaseManager
clientIdPath string
clientSecretPath string
newAccessTokenNotificationChan chan bool
@ -302,8 +446,8 @@ type TokenManager struct {
exitAfterAuth bool
}
func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *TokenManager {
return &TokenManager{
func NewAgentManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *AgentManager {
return &AgentManager{
filePaths: fileDeposits,
templates: templates,
clientIdPath: clientIdPath,
@ -315,7 +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()
defer tm.mutex.Unlock()
@ -326,7 +470,7 @@ func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, acc
tm.newAccessTokenNotificationChan <- true
}
func (tm *TokenManager) GetToken() string {
func (tm *AgentManager) GetToken() string {
tm.mutex.Lock()
defer tm.mutex.Unlock()
@ -334,7 +478,7 @@ func (tm *TokenManager) GetToken() string {
}
// 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")
if clientID == "" {
clientIDAsByte, err := ReadFile(tm.clientIdPath)
@ -384,7 +528,7 @@ func (tm *TokenManager) FetchNewAccessToken() error {
}
// Refreshes the existing access token
func (tm *TokenManager) RefreshAccessToken() error {
func (tm *AgentManager) RefreshAccessToken() error {
httpClient := resty.New()
httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second).
@ -405,7 +549,7 @@ func (tm *TokenManager) RefreshAccessToken() error {
return nil
}
func (tm *TokenManager) ManageTokenLifecycle() {
func (tm *AgentManager) ManageTokenLifecycle() {
for {
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
accessTokenRefreshedTime := tm.accessTokenRefreshedTime
@ -473,7 +617,7 @@ func (tm *TokenManager) ManageTokenLifecycle() {
}
}
func (tm *TokenManager) WriteTokenToFiles() {
func (tm *AgentManager) WriteTokenToFiles() {
token := tm.GetToken()
for _, sinkFile := range tm.filePaths {
if sinkFile.Type == "file" {
@ -490,7 +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 {
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
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)
}
func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan chan os.Signal) {
func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId int, sigChan chan os.Signal) {
pollingInterval := time.Duration(5 * time.Minute)
@ -523,47 +667,61 @@ func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan ch
execCommand := secretTemplate.Config.Execute.Command
for {
token := tm.GetToken()
select {
case <-sigChan:
return
default:
{
tm.dynamicSecretLeases.Prune()
token := tm.GetToken()
if token != "" {
var processedTemplate *bytes.Buffer
var err error
if token != "" {
if secretTemplate.SourcePath != "" {
processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
} else {
processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
}
var processedTemplate *bytes.Buffer
var err error
if err != nil {
log.Error().Msgf("unable to process template because %v", err)
} else {
if (existingEtag != currentEtag) || firstRun {
if secretTemplate.SourcePath != "" {
processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag)
} else {
processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag)
}
tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
existingEtag = currentEtag
if err != nil {
log.Error().Msgf("unable to process template because %v", err)
} else {
if (existingEtag != currentEtag) || firstRun {
if !firstRun && execCommand != "" {
log.Info().Msgf("executing command: %s", execCommand)
err := ExecuteCommandWithTimeout(execCommand, execTimeout)
tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
existingEtag = currentEtag
if err != nil {
log.Error().Msgf("unable to execute command because %v", err)
}
if !firstRun && execCommand != "" {
log.Info().Msgf("executing command: %s", execCommand)
err := ExecuteCommandWithTimeout(execCommand, execTimeout)
if err != nil {
log.Error().Msgf("unable to execute command because %v", err)
}
if firstRun {
firstRun = false
}
}
}
// now the idea is we pick the next sleep time in which the one shorter out of
// - polling time
// - first lease that's gonna get expired in the template
firstLeaseExpiry, isValid := tm.dynamicSecretLeases.GetFirstExpiringLeaseTime(templateId)
var waitTime = pollingInterval
if isValid && firstLeaseExpiry.Sub(time.Now()) < pollingInterval {
waitTime = firstLeaseExpiry.Sub(time.Now())
}
if firstRun {
firstRun = false
}
time.Sleep(waitTime)
} else {
// It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
time.Sleep(3 * time.Second)
}
}
time.Sleep(pollingInterval)
} else {
// It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
time.Sleep(3 * time.Second)
}
}
}
@ -645,13 +803,14 @@ var agentCmd = &cobra.Command{
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
filePaths := agentConfig.Sinks
tm := NewTokenManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth)
tm := NewAgentManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth)
tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan)
go tm.ManageTokenLifecycle()
for i, template := range agentConfig.Templates {
log.Info().Msgf("template engine started for template %v...", i+1)
go tm.MonitorSecretChanges(template, sigChan)
go tm.MonitorSecretChanges(template, i, sigChan)
}
for {

View File

@ -297,7 +297,6 @@ var secretsSetCmd = &cobra.Command{
updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName,
SecretName: secret.PlainTextKey,
SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag,
@ -305,7 +304,7 @@ var secretsSetCmd = &cobra.Command{
SecretPath: secretsPath,
}
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
if err != nil {
util.HandleError(err, "Unable to process secret update request")
return
@ -419,7 +418,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
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 {
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")
}
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 {
util.HandleError(err, "To fetch all secrets")
}

View File

@ -1,5 +1,7 @@
package models
import "time"
type UserCredentials struct {
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
@ -40,6 +42,23 @@ type PlaintextSecretResult struct {
Etag string
}
type DynamicSecret struct {
Id string `json:"id"`
DefaultTTL string `json:"defaultTTL"`
MaxTTL string `json:"maxTTL"`
Type string `json:"type"`
}
type DynamicSecretLease struct {
Lease struct {
Id string `json:"id"`
ExpireAt time.Time `json:"expireAt"`
} `json:"lease"`
DynamicSecret DynamicSecret `json:"dynamicSecret"`
// this is a varying dict based on provider
Data map[string]interface{} `json:"data"`
}
type SingleFolder struct {
ID string `json:"_id"`
Name string `json:"name"`

View File

@ -195,6 +195,31 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
}, nil
}
func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) {
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
dynamicSecretRequest := api.CreateDynamicSecretLeaseV1Request{
ProjectSlug: projectSlug,
Environment: environmentName,
SecretPath: secretsPath,
Slug: slug,
TTL: ttl,
}
dynamicSecret, err := api.CallCreateDynamicSecretLeaseV1(httpClient, dynamicSecretRequest)
if err != nil {
return models.DynamicSecretLease{}, err
}
return models.DynamicSecretLease{
Lease: dynamicSecret.Lease,
Data: dynamicSecret.Data,
DynamicSecret: dynamicSecret.DynamicSecret,
}, nil
}
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil

View File

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/workspace/{projectId}/tags"
---

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/workspace/{projectId}/tags"
---

View File

@ -0,0 +1,4 @@
---
title: "Attach tags"
openapi: "POST /api/v3/secrets/tags/{secretName}"
---

View File

@ -0,0 +1,4 @@
---
title: "Detach tags"
openapi: "DELETE /api/v3/secrets/tags/{secretName}"
---

View File

@ -1,37 +1,102 @@
---
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
2. Infisical will connect with your database with admin access
3. If last rotated one was username1, then username2 is chosen to be rotated
5. Update it's password with random value
6. After testing it gets saved to the provided secret mapping
1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`.
2. Create another MySQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html).
## How it works
1. Infisical connects to your database using the provided `admin` user account.
2. A random value is generated and the password for `user-a` is updated with the new value.
3. The new password is then tested by logging into the database
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
5. The process is then repeated for `user-b` on the next rotation.
6. The cycle repeats until secret rotation is deleted/stopped.
## Rotation Configuration
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
2. Click on `MySQL`
3. Provide the inputs
- Admin Username: DB admin username
- Admin Password: DB admin password
- Host: DB host
- Port: DB port(number)
- Username1: The first username in two to rotate
- Username2: The second username in two to rotate
- CA: Certificate to connect with database(string)
4. Final step
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- Finally select the secrets in your provided board to replace with new secret after each rotation
- Your done and good to go.
<Steps>
<Step title="Open Secret Rotation Page">
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
</Step>
<Step title="Click on MySQL card" />
<Step title="Provide the inputs">
<ParamField path="Admin Username" type="string" required>
Rotator admin username
</ParamField>
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>

View File

@ -1,33 +1,104 @@
---
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.
2. Infisical will connect with your database with admin access.
3. If last rotated one was username1, then username2 is chosen to be rotated.
5. Update it's password with random value.
6. After testing it gets saved to the provided secret mapping.
## Prerequisite
1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`.
2. Create another PostgreSQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
To learn more about Postgres permission system, please visit this [documentation](https://www.postgresql.org/docs/9.1/sql-grant.html).
## How it works
1. Infisical connects to your database using the provided `admin` user account.
2. A random value is generated and the password for `user-a` is updated with the new value.
3. The new password is then tested by logging into the database
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
5. The process is then repeated for `user-b` on the next rotation.
6. The cycle repeats until secret rotation is deleted/stopped.
## Rotation Configuration
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
2. Click on `PostgreSQL`
3. Provide the inputs
- Admin Username: DB admin username
- Admin Password: DB admin password
- Host: DB host
- Port: DB port(number)
- Username1: The first username in two to rotate
- Username2: The second username in two to rotate
- CA: Certificate to connect with database(string)
4. Final step
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- Finally select the secrets in your provided board to replace with new secret after each rotation
- Your done and good to go.
<Steps>
<Step title="Open Secret Rotation Page">
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
</Step>
<Step title="Click on PostgresSQL card" />
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>

View File

@ -1,31 +1,58 @@
---
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.
It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys.
Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical.
## Working
## Prerequisite
1. Infisical will need an admin token of SendGrid to create API keys dynamically.
2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically
3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys)
You will need a valid SendGrid admin key with the necessary scope to create additional API keys.
Follow the [SendGrid Docs to create an admin api key](https://docs.sendgrid.com/ui/account-and-settings/api-keys)
## How it works
Using the provided admin API key, Infisical will attempt to create child API keys with the specified permissions.
New keys will ge generated every time a rotation occurs. Behind the scenes, Infisical uses the [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) to generate new API keys.
## Rotation Configuration
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
2. Click on `Twilio SendGrid Card`
3. Provide the inputs
- Admin API Key:
SendGrid admin key to create lower scoped API keys.
- API Key Scopes
SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions)
<Steps>
<Step title="Open Secret Rotation Page">
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
</Step>
<Step title="Click on Twilio SendGrid Card" />
<Step title="Provide the inputs">
<ParamField path="Admin API Key" type="string" required>
SendGrid admin API key with permission to create child scoped API keys.
</ParamField>
4. Final step
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- Finally select the secrets in your provided board to replace with new secret after each rotation
- Your done and good to go.
Now your output mapped secret value will be replaced periodically by SendGrid.
<ParamField path="Admin API Key" type="array" required>
The permissions that the newly generated API keys will have. To view possible permissions, visit [this documentation](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions).
Permissions must be entered as a list of strings.
Example: `["user.profile.read", "user.profile.update"]`
</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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 KiB

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme
## Install Operator
The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
<Tabs>
<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
kind: InfisicalSecret
metadata:
# Name of of this InfisicalSecret resource
name: infisicalsecret-sample
name: infisicalsecret-sample
labels:
label-to-be-passed-to-managed-secret: sample-value
annotations:
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
spec:
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
hostAPI: https://app.infisical.com/api
resyncInterval: 60
authentication:
serviceToken:
serviceTokenSecretReference:
secretName: service-token
hostAPI: https://app.infisical.com/api
resyncInterval: 10
authentication:
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
# If you have multiple authentication methods defined, it may cause issues.
universalAuth:
secretsScope:
projectSlug: <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
secretsScope:
envSlug: dev
secretsPath: "/"
managedSecretReference:
secretName: managed-secret # <-- the name of kubernetes secret that will be created
secretNamespace: default # <-- where the kubernetes secret should be created
# secretType: kubernetes.io/dockerconfigjson
```
### InfisicalSecret CRD properties
@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute.
</Accordion>
<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 title="authentication.serviceToken.serviceTokenSecretReference">
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.
<Accordion title="authentication.universalAuth">
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.
#### 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.
``` 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
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
```yaml
apiVersion: secrets.infisical.com/v1alpha1
@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute.
serviceTokenSecretReference:
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
secretsScope:
envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: <secrets-path> # Root is "/"
...
```
</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">
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.

View File

@ -87,13 +87,23 @@ Since these encryption operations occur on the client-side, the Infisical API is
### High availability
Infisical leverages the robust container orchestration capabilities of Kubernetes and the inherent high availability features of the storage backend (i.e. Bitnami MongoDB) to ensure resilience and fault tolerance.
Infisical Cloud utilizes several strategies to ensure high availability, leveraging AWS services to maintain continuous operation and data integrity.
- Kubernetes: By deploying multiple replicas of Infisical application on Kubernetes, operations continue even if a single instance fails. Kubernetes Services facilitate load balancing, effectively distributing traffic across your applications instances and ensuring optimal performance.
- Storage backend: Bitnami MongoDB supports replica sets, which provide data redundancy and automatic failover for the underlying database.
- If using [Infisical Cloud](https://app.infisical.com), data is stored in a Mongo Atlas cluster with storage autoscaling and cluster tier autoscaling enabled; as you'd expect, the cluster sits on a dedicated node.
#### Multi-AZ AWS RDS
Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments.
This configuration ensures that the database service is highly available and durable.
AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ).
This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption.
The continuous backup and replication to the standby instance safeguard data against loss and ensure its availability even during system failures.
Together, Kubernetes self-healing mechanisms and Bitnami MongoDBs 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

View File

@ -467,6 +467,14 @@
"api-reference/endpoints/folders/delete"
]
},
{
"group": "Secret tags",
"pages": [
"api-reference/endpoints/secret-tags/list",
"api-reference/endpoints/secret-tags/create",
"api-reference/endpoints/secret-tags/delete"
]
},
{
"group": "Secrets",
"pages": [
@ -474,7 +482,9 @@
"api-reference/endpoints/secrets/create",
"api-reference/endpoints/secrets/read",
"api-reference/endpoints/secrets/update",
"api-reference/endpoints/secrets/delete"
"api-reference/endpoints/secrets/delete",
"api-reference/endpoints/secrets/attach-tags",
"api-reference/endpoints/secrets/detach-tags"
]
},
{

View File

@ -1,15 +1,15 @@
---
title: "Using Infisical EE"
title: "Infisical enterprise"
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.
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>
<Step title="Purchase a license">
Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting team@infisical.com to purchase a license.
Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting sales@infisical.com to purchase a license.
Once purchased, you will be issued a license key.
</Step>
@ -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 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 youll be able to use the paid features.
<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>
Once your instance starts up, the license key will be validated and youll 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>
</Steps>
</Steps>

View File

@ -29,6 +29,7 @@ module.exports = {
},
plugins: ["react", "prettier", "simple-import-sort", "import"],
rules: {
"@typescript-eslint/no-empty-function": "off",
quotes: ["error", "double", { avoidEscape: true }],
"comma-dangle": ["error", "only-multiline"],
"react/react-in-jsx-scope": "off",
@ -72,7 +73,6 @@ module.exports = {
],
"@typescript-eslint/no-non-null-assertion": "off",
"simple-import-sort/exports": "warn",
"@typescript-eslint/no-empty-function": "off",
"simple-import-sort/imports": [
"warn",
{

View File

@ -1,28 +1,28 @@
const path = require('path');
const path = require("path");
module.exports = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'storybook-dark-mode',
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"storybook-dark-mode",
{
name: '@storybook/addon-styling',
name: "@storybook/addon-styling",
options: {
postCss: {
implementation: require('postcss')
implementation: require("postcss")
}
}
}
],
framework: {
name: '@storybook/nextjs',
name: "@storybook/nextjs",
options: {}
},
core: {
disableTelemetry: true
},
docs: {
autodocs: 'tag'
autodocs: "tag"
}
};

View File

@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
@ -66,6 +67,7 @@
"jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0",
"markdown-it": "^13.0.1",
"ms": "^2.1.3",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
@ -83,6 +85,7 @@
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
"set-cookie-parser": "^2.5.1",
"sharp": "^0.33.2",
@ -9911,13 +9914,13 @@
"dev": true
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
@ -9925,7 +9928,7 @@
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
@ -10874,9 +10877,9 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"dev": true,
"engines": {
"node": ">= 0.6"
@ -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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
@ -13255,17 +13263,17 @@
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@ -13759,9 +13767,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@ -15126,9 +15134,9 @@
}
},
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
"dev": true
},
"node_modules/ipaddr.js": {
@ -17518,9 +17526,9 @@
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/multipipe": {
"version": "1.0.2",
@ -19786,9 +19794,9 @@
}
},
"node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
@ -20293,6 +20301,26 @@
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
}
},
"node_modules/react-toastify": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz",
"integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==",
"dependencies": {
"clsx": "^1.1.1"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-toastify/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": {
"node": ">=6"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -21287,12 +21315,6 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@ -23700,9 +23722,9 @@
}
},
"node_modules/webpack-dev-middleware": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz",
"integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz",
"integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==",
"dev": true,
"dependencies": {
"colorette": "^2.0.10",

View File

@ -74,6 +74,7 @@
"jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0",
"markdown-it": "^13.0.1",
"ms": "^2.1.3",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
@ -91,6 +92,7 @@
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
"set-cookie-parser": "^2.5.1",
"sharp": "^0.33.2",

View File

@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config";
export const initPostHog = () => {
// @ts-ignore
console.log("Hi there 👋")
console.log("Hi there 👋");
try {
if (typeof window !== "undefined") {
// @ts-ignore
@ -19,7 +19,7 @@ export const initPostHog = () => {
return posthog;
} catch (e) {
console.log("posthog err", e)
console.log("posthog err", e);
}
return undefined;

View File

@ -3,9 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const Error = ({ text }: { text: string }): JSX.Element => {
return (
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red mt-1.5 mb-2 mx-2" />
{text && <p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>}
<div className="relative m-auto flex w-fit flex-row items-center justify-center rounded-full">
<FontAwesomeIcon icon={faExclamationTriangle} className="mx-2 mt-1.5 mb-2 text-red" />
{text && <p className="relative top-0 mr-2 py-1 text-sm text-red">{text}</p>}
</div>
);
};

View File

@ -39,16 +39,16 @@ const InputField = ({
if (isStatic === true) {
return (
<div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md">
<p className="text-sm font-semibold text-gray-400 mb-0.5">{label}</p>
{text && <p className="text-xs text-gray-400 mb-2">{text}</p>}
<div className="my-2 flex w-full max-w-md flex-col justify-center md:my-4">
<p className="mb-0.5 text-sm font-semibold text-gray-400">{label}</p>
{text && <p className="mb-2 text-xs text-gray-400">{text}</p>}
<input
onChange={(e) => onChangeHandler(e.target.value)}
type={type}
placeholder={placeholder}
value={value}
required={isRequired}
className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none"
className="text-md min-w-16 w-full rounded-md border border-gray-600 bg-bunker-800 p-2 text-gray-400 outline-none"
name={name}
readOnly
autoComplete={autoComplete}
@ -58,12 +58,12 @@ const InputField = ({
);
}
return (
<div className="flex-col w-full">
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
<p className="text-sm font-semibold mr-1">{label}</p>
<div className="w-full flex-col">
<div className="mb-0.5 flex flex-row items-center text-mineshaft-300">
<p className="mr-1 text-sm font-semibold">{label}</p>
</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"
} rounded-md`}
>
@ -75,11 +75,11 @@ const InputField = ({
required={isRequired}
className={`${
blurred
? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400"
? "text-bunker-800 focus:text-gray-400 active:text-gray-400 group-hover:text-gray-400"
: ""
} ${
error ? "focus:ring-red/50" : "focus:ring-primary/50"
} relative peer bg-mineshaft-900 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
} text-md min-w-16 peer relative w-full rounded-md bg-mineshaft-900 p-2 text-gray-400 outline-none duration-200 focus:ring-4`}
name={name}
spellCheck="false"
autoComplete={autoComplete}
@ -91,7 +91,7 @@ const InputField = ({
onClick={() => {
setPasswordVisible(!passwordVisible);
}}
className="absolute self-end mr-3 text-gray-400 cursor-pointer"
className="absolute mr-3 cursor-pointer self-end text-gray-400"
>
{passwordVisible ? (
<FontAwesomeIcon icon={faEyeSlash} />
@ -101,7 +101,7 @@ const InputField = ({
</button>
)}
{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" />
{value
.split("")
@ -109,7 +109,7 @@ const InputField = ({
.map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className="text-xxs mx-0.5"
className="mx-0.5 text-xxs"
icon={faCircle}
/>
))}
@ -121,7 +121,7 @@ const InputField = ({
</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>
);
};

View File

@ -34,19 +34,19 @@ const ListBox = ({
<Listbox value={isSelected} onChange={onChange}>
<div className="relative">
<Listbox.Button
className={`text-gray-400 relative ${
className={`relative text-gray-400 ${
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">
{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}
</span>
</div>
{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" />
</div>
)}
@ -58,16 +58,16 @@ const ListBox = ({
leaveFrom="opacity-100"
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) => (
<Listbox.Option
key={`${person}.${personIdx + 1}`}
className={({ active, selected }) =>
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
selected ? "bg-white/10 text-gray-400 font-bold" : ""
`relative my-0.5 cursor-default select-none rounded-md py-2 pl-10 pr-4 ${
selected ? "bg-white/10 font-bold text-gray-400" : ""
} ${
active && !selected
? "bg-white/5 text-mineshaft-200 cursor-pointer"
? "cursor-pointer bg-white/5 text-mineshaft-200"
: "text-gray-400"
} `
}
@ -83,7 +83,7 @@ const ListBox = ({
{person}
</span>
{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" />
</span>
) : null}
@ -92,9 +92,9 @@ const ListBox = ({
</Listbox.Option>
))}
{buttonAction && (
<button type="button" onClick={buttonAction} className="cursor-pointer w-full">
<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">
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
<button type="button" onClick={buttonAction} className="w-full cursor-pointer">
<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="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
<FontAwesomeIcon icon={faPlus} className="text-lg" />
</span>
Add Project

View File

@ -43,7 +43,7 @@ const Button = ({
loading,
icon,
iconDisabled,
type = "button",
type = "button"
}: ButtonProps): JSX.Element => {
// Check if the button show always be 'active' - then true;
// 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",
// 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 === "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 === "red" && "bg-red-800 border border-red",
@ -78,7 +82,9 @@ const Button = ({
color !== "mineshaft" && color !== "red" && color !== "none" && "text-black",
color === "red" && "text-gray-200",
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"
);
@ -103,7 +109,7 @@ const Button = ({
<div
className={`${
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
src="/images/loading/loadingblack.gif"
@ -116,7 +122,7 @@ const Button = ({
{icon && (
<FontAwesomeIcon
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"
}`}
/>
@ -124,7 +130,7 @@ const Button = ({
{iconDisabled && (
<FontAwesomeIcon
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"
}`}
/>

View File

@ -64,7 +64,7 @@ const AddProjectMemberDialog = ({
) : (
<Dialog.Title
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")}
</Dialog.Title>
@ -127,7 +127,9 @@ const AddProjectMemberDialog = ({
</div>
) : (
<Button
onButtonPressed={() => router.push(`/org/${localStorage.getItem("orgData.id")}/members`)}
onButtonPressed={() =>
router.push(`/org/${localStorage.getItem("orgData.id")}/members`)
}
color="mineshaft"
text={t("section.members.add-dialog.add-user-to-org") as string}
size="md"

View File

@ -28,11 +28,11 @@ export const AddUpdateEnvironmentDialog = ({
onCreateSubmit,
onEditSubmit,
initialValues,
isEditMode,
isEditMode
}: Props) => {
const [formInput, setFormInput] = useState<FormFields>({
name: "",
slug: "",
slug: ""
});
// This use effect can be removed when the unmount is happening from outside the component
@ -50,7 +50,7 @@ export const AddUpdateEnvironmentDialog = ({
e.preventDefault();
const data = {
name: formInput.name,
slug: formInput.slug.toLowerCase(),
slug: formInput.slug.toLowerCase()
};
if (isEditMode) {
onEditSubmit(data);
@ -62,75 +62,70 @@ export const AddUpdateEnvironmentDialog = ({
return (
<div>
<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
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-out duration-150'
leaveFrom='opacity-100'
leaveTo='opacity-0'
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-out duration-150"
leaveFrom="opacity-100"
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>
<div className='fixed inset-0 overflow-y-auto z-50'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
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.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
{isEditMode
? "Update environment"
: "Create a new environment"}
<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 as="h3" className="text-lg font-medium leading-6 text-gray-400">
{isEditMode ? "Update environment" : "Create a new environment"}
</Dialog.Title>
<form onSubmit={onFormSubmit}>
<div className='max-h-28 mt-4'>
<div className="mt-4 max-h-28">
<InputField
label='Environment Name'
label="Environment Name"
onChangeHandler={(val) => onInputChange("name", val)}
type='varName'
type="varName"
value={formInput.name}
placeholder=''
placeholder=""
isRequired
// error={error.length > 0}
// errorText={error}
/>
</div>
<div className='max-h-28 mt-4'>
<div className="mt-4 max-h-28">
<InputField
label='Environment Slug'
label="Environment Slug"
onChangeHandler={(val) => onInputChange("slug", val)}
type='varName'
type="varName"
value={formInput.slug}
placeholder=''
placeholder=""
isRequired
// error={error.length > 0}
// errorText={error}
/>
</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
</p>
<div className='mt-4 max-w-min'>
<div className="mt-4 max-w-min">
<Button
onButtonPressed={() => null}
type='submit'
color='mineshaft'
type="submit"
color="mineshaft"
text={isEditMode ? "Update" : "Create"}
active={formInput.name !== "" && formInput.slug !== ""}
size='md'
size="md"
/>
</div>
</form>

View File

@ -13,76 +13,63 @@ type Props = {
orgName: string;
};
const AddUserDialog = ({
isOpen,
closeModal,
submitModal,
email,
setEmail,
orgName,
}: Props) => {
const AddUserDialog = ({ isOpen, closeModal, submitModal, email, setEmail, orgName }: Props) => {
const submit = () => {
submitModal(email);
};
return (
<div className='z-50'>
<div className="z-50">
<Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative' onClose={closeModal}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-200'
leaveFrom='opacity-100'
leaveTo='opacity-0'
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
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>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
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
as='h3'
className='text-lg font-medium leading-6 text-gray-400 z-50'
as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400"
>
Invite others to {orgName}
</Dialog.Title>
<div className='mt-2 mb-4'>
<p className='text-sm text-gray-500'>
An invite is specific to an email address and expires
after 1 day. For security reasons, you will need to
separately add members to projects.
<div className="mt-2 mb-4">
<p className="text-sm text-gray-500">
An invite is specific to an email address and expires after 1 day. For
security reasons, you will need to separately add members to projects.
</p>
</div>
<div className='max-h-28'>
<div className="max-h-28">
<InputField
label='Email'
label="Email"
onChangeHandler={setEmail}
type='varName'
type="varName"
value={email}
placeholder=''
placeholder=""
isRequired
/>
</div>
<div className='mt-4 max-w-max'>
<Button
onButtonPressed={submit}
color='mineshaft'
text='Invite'
size='md'
/>
<div className="mt-4 max-w-max">
<Button onButtonPressed={submit} color="mineshaft" text="Invite" size="md" />
</div>
</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">

View File

@ -5,7 +5,6 @@ import Button from "../buttons/Button";
import InputField from "../InputField";
import { Checkbox } from "../table/Checkbox";
type Props = {
isOpen: boolean;
closeModal: () => void;
@ -26,8 +25,8 @@ const AddWorkspaceDialog = ({
workspaceName,
setWorkspaceName,
error,
loading,
}:Props) => {
loading
}: Props) => {
const [addAllUsers, setAddAllUsers] = useState(true);
const submit = () => {
submitModal(workspaceName, addAllUsers);
@ -60,11 +59,8 @@ const AddWorkspaceDialog = ({
leaveFrom="opacity-100 scale-100"
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.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
<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 as="h3" className="text-lg font-medium leading-6 text-gray-400">
Create a new project
</Dialog.Title>
<div className="mt-2">
@ -72,7 +68,7 @@ const AddWorkspaceDialog = ({
This project will contain your secrets and configs.
</p>
</div>
<div className="max-h-28 mt-4">
<div className="mt-4 max-h-28">
<InputField
label="Project Name"
onChangeHandler={setWorkspaceName}
@ -84,10 +80,7 @@ const AddWorkspaceDialog = ({
/>
</div>
<div className="mt-4 ml-1">
<Checkbox
addAllUsers={addAllUsers}
setAddAllUsers={setAddAllUsers}
/>
<Checkbox addAllUsers={addAllUsers} setAddAllUsers={setAddAllUsers} />
</div>
<div className="mt-4 max-w-min">
<Button

View File

@ -3,89 +3,76 @@ import { Dialog, Transition } from "@headlessui/react";
import InputField from "../InputField";
// REFACTOR: Move all these modals into one reusable one
// REFACTOR: Move all these modals into one reusable one
type Props = {
isOpen?: boolean;
onClose: ()=>void;
onClose: () => void;
title: string;
onSubmit:()=>void;
deleteKey?:string;
}
onSubmit: () => void;
deleteKey?: string;
};
const DeleteActionModal = ({
isOpen,
onClose,
title,
onSubmit,
deleteKey
}:Props) => {
const [deleteInputField, setDeleteInputField] = useState("")
const DeleteActionModal = ({ isOpen, onClose, title, onSubmit, deleteKey }: Props) => {
const [deleteInputField, setDeleteInputField] = useState("");
useEffect(() => {
setDeleteInputField("");
}, [isOpen]);
useEffect(() => {
setDeleteInputField("");
}, [isOpen]);
return (
<div>
<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
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-150'
leaveFrom='opacity-100'
leaveTo='opacity-0'
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-150"
leaveFrom="opacity-100"
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>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
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.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
<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 as="h3" className="text-lg font-medium leading-6 text-gray-400">
{title}
</Dialog.Title>
<div className='mt-2'>
<p className='text-sm text-gray-500'>
This action is irrevertible.
</p>
<div className="mt-2">
<p className="text-sm text-gray-500">This action is irrevertible.</p>
</div>
<div className='mt-2'>
<div className="mt-2">
<InputField
isRequired
label={`Type ${deleteKey} to delete the resource`}
onChangeHandler={(val) => setDeleteInputField(val)}
value={deleteInputField}
type='text'
type="text"
/>
</div>
<div className='mt-6'>
<div className="mt-6">
<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'
type="button"
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}
disabled={
Boolean(deleteKey) && deleteInputField !== deleteKey
}
disabled={Boolean(deleteKey) && deleteInputField !== deleteKey}
>
Delete
</button>
<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'
type="button"
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}
>
Cancel

View File

@ -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.
type Props = {
isOpen: boolean
onClose: () => void
onSubmit: () => void
}
isOpen: boolean;
onClose: () => void;
onSubmit: () => void;
};
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
const { t } = useTranslation()
const { t } = useTranslation();
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
@ -45,7 +45,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
leaveFrom="opacity-100 scale-100"
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">
{t("dashboard:sidebar.delete-key-dialog.title")}
</Dialog.Title>
@ -57,14 +57,14 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
<div className="mt-6 flex justify-start">
<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}
>
Delete
</button>
<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}
>
Cancel

View File

@ -10,66 +10,55 @@ type Props = {
userIdToBeDeleted: string;
};
const DeleteUserDialog = ({
isOpen,
closeModal,
submitModal,
userIdToBeDeleted,
}: Props) => {
const DeleteUserDialog = ({ isOpen, closeModal, submitModal, userIdToBeDeleted }: Props) => {
const submit = () => {
submitModal(userIdToBeDeleted);
};
return (
<div>
<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
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-200'
leaveFrom='opacity-100'
leaveTo='opacity-0'
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
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>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
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.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
Are you sure you want to remove this user from the
workspace?
<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 as="h3" className="text-lg font-medium leading-6 text-gray-400">
Are you sure you want to remove this user from the workspace?
</Dialog.Title>
<div className='mt-2'>
<p className='text-sm text-gray-500'>
This action is irrevertible.
</p>
<div className="mt-2">
<p className="text-sm text-gray-500">This action is irrevertible.</p>
</div>
<div className='mt-6'>
<div className="mt-6">
<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'
type="button"
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}
>
Delete
</button>
<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'
type="button"
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}
>
Cancel

View File

@ -34,27 +34,27 @@ const BottonRightPopup = ({
}: PopupProps): JSX.Element => {
return (
<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"
>
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
<div className="flex w-full flex-row items-center justify-between border-b border-gray-600/70 px-6 pb-3">
<div className="mr-2 mt-0.5 flex flex-row text-xl font-bold">
<div>{titleText}</div>
<div className="ml-2.5">{emoji}</div>
</div>
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
<FontAwesomeIcon
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>
</div>
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">{textLine1}</div>
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
<div className="flex flex-row px-6 w-full">
<div className="mt-4 mb-0.5 block px-6 text-gray-300 sm:inline">{textLine1}</div>
<div className="mb-4 block px-6 sm:inline">{textLine2}</div>
<div className="flex w-full flex-row px-6">
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<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}
target="_blank"
rel="noopener"

View File

@ -9,7 +9,7 @@ export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
{addAllUsers === true ? (
<input
type="checkbox"
className="accent-primary h-4 w-4"
className="h-4 w-4 accent-primary"
checked
readOnly
onClick={() => setAddAllUsers(!addAllUsers)}
@ -20,12 +20,12 @@ export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
role="button"
tabIndex={0}
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)}
/>
)}
<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.
</label>
</div>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -30,9 +30,9 @@ const ConfirmEnvOverwriteModal = ({
onClose={onClose}
>
<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-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>
</ModalContent>
</Modal>

Some files were not shown because too many files have changed in this diff Show More