Compare commits

...

65 Commits

Author SHA1 Message Date
4a35623956 remove for of with for await 2024-05-15 20:19:10 -04:00
74fe673724 patch project identity update 2024-05-15 20:12:45 -04:00
399ca7a221 Merge pull request #1826 from justin1121/patch-1
Update secret-versioning.mdx
2024-05-15 15:34:03 +05:30
4afd95fe1a Merge pull request #1825 from akhilmhdh/feat/sync-integration-inline
Secret reference and integration sync support
2024-05-15 01:36:19 -04:00
3cd719f6b0 update index secret references button 2024-05-15 09:57:24 +05:30
c6352cc970 updated texts and comments 2024-05-15 09:57:24 +05:30
=
d4555f9698 feat: ui for reindex secret reference 2024-05-15 09:57:24 +05:30
=
393964c4ae feat: implemented inline secret reference integration sync 2024-05-15 09:57:23 +05:30
c438479246 update prod pipeline names 2024-05-14 16:14:42 -04:00
9828cbbfbe Update secret-versioning.mdx 2024-05-14 16:28:43 -03:00
fc1dffd7e2 Merge pull request #1823 from Infisical/snyk-fix-a2a4b055e42c14d5cbdb505e7670d300
[Snyk] Security upgrade bullmq from 5.3.3 to 5.4.2
2024-05-14 12:02:13 -04:00
55f8198a2d Merge pull request #1821 from matthewaerose/patch-1
Fix: Correct typo from 'Halm' to 'Helm'
2024-05-14 11:46:49 -04:00
4d166402df Merge pull request #1824 from Infisical/create-pull-request/patch-1715660210
GH Action: rename new migration file timestamp
2024-05-14 00:17:34 -04:00
19edf83dbc chore: renamed new migration files to latest timestamp (gh-action) 2024-05-14 04:16:49 +00:00
13f6b238e7 fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-BRACES-6838727
- https://snyk.io/vuln/SNYK-JS-MICROMATCH-6838728
2024-05-14 04:16:40 +00:00
8dee1f8fc7 Merge pull request #1800 from Infisical/gcp-iam-auth
GCP Native Authentication Method
2024-05-14 00:16:28 -04:00
3b23035dfb disable secret scanning 2024-05-13 23:12:36 -04:00
0c8ef13d8d Fix: Correct typo from 'Halm' to 'Helm' 2024-05-13 13:38:09 -05:00
389d51fa5c Merge pull request #1819 from akhilmhdh/feat/hide-secret-scanner
feat: added secret-scanning disable option
2024-05-13 13:53:35 -04:00
638208e9fa update secret scanning text 2024-05-13 13:48:23 -04:00
c176d1e4f7 Merge pull request #1818 from akhilmhdh/fix/patches-v2
Improvised secret input component and fontawesome performance improvment
2024-05-13 13:42:30 -04:00
=
91a23a608e feat: added secret-scanning disable option 2024-05-13 21:55:37 +05:30
=
c6a25271dd fix: changed cross key to check for submission for save secret changes 2024-05-13 19:50:38 +05:30
=
0f5c1340d3 feat: dashboard optimized on font awesome levels using symbols technique 2024-05-13 13:40:59 +05:30
=
ecbdae110d feat: simplified secret input with auto completion 2024-05-13 13:40:59 +05:30
=
8ef727b4ec fix: resolved typo in dashboard nav header redirection 2024-05-13 13:40:59 +05:30
=
c6f24dbb5e fix: resolved unique key error secret input rendering 2024-05-13 13:40:59 +05:30
18c0d2fd6f Merge pull request #1814 from Infisical/aws-integration-patch
Allow updating tags in AWS Secret Manager integration
2024-05-12 15:03:19 -07:00
c1fb8f47bf Add UntagResource IAM policy requirement for AWS SM integration docs 2024-05-12 08:57:41 -07:00
990eddeb32 Merge pull request #1816 from akhilmhdh/fix/remove-migration-notice
fix: removed migration notice
2024-05-11 13:43:04 -04:00
=
ce01f8d099 fix: removed migration notice 2024-05-11 23:04:43 +05:30
faf6708b00 Merge pull request #1815 from akhilmhdh/fix/migration-mode-patch-v1
feat: maintaince mode enable machine identity login and renew
2024-05-11 11:26:21 -04:00
=
a58d6ebdac feat: maintaince mode enable machine identity login and renew 2024-05-11 20:54:00 +05:30
818b136836 Make app and appId optional in update integration endpoint 2024-05-10 19:17:40 -07:00
0cdade6a2d Update AWS SM integration to allow updating tags 2024-05-10 19:07:44 -07:00
bcf9b68e2b Update GCP auth method description 2024-05-10 10:28:29 -07:00
6aa9fb6ecd Updated docs 2024-05-10 10:24:29 -07:00
38e7382d85 Remove GCP audit log space 2024-05-10 10:15:43 -07:00
95e12287c2 Minor edits to renaming GCE -> ID Token 2024-05-10 10:14:13 -07:00
c6d14a4bea Update 2024-05-10 10:10:51 -07:00
0a91586904 Remove service account JSON requirement from GCP Auth 2024-05-10 09:56:35 -07:00
6561a9c7be Merge pull request #1804 from Infisical/feat/add-support-for-secret-folder-rename-overview
Feature: add support for secret folder rename in the overview page
2024-05-10 23:07:14 +08:00
86aaa486b4 Update secret-folder-service.ts 2024-05-10 17:00:30 +02:00
9880977098 misc: addressed naming suggestion 2024-05-10 22:52:08 +08:00
b93aaffe77 adjustment: updated to use project slug 2024-05-10 22:34:16 +08:00
1ea0d55dd1 Merge pull request #1813 from Infisical/misc/update-documentation-for-github-integration
misc: updated documentation for github integration to include official action
2024-05-10 09:14:14 -04:00
3fff272cb3 feat: added snapshot for batch 2024-05-10 15:46:31 +08:00
2559809eac misc: addressed formatting issues 2024-05-10 14:41:35 +08:00
f32abbdc25 feat: integrate overview folder rename with new batch endpoint 2024-05-10 14:00:49 +08:00
a6f750fafb feat: added batch update endpoint for folders 2024-05-10 13:57:00 +08:00
610f474ecc Rename migration file 2024-05-09 16:58:39 -07:00
03f4a699e6 Improve GCP docs 2024-05-09 16:53:08 -07:00
533d49304a Update GCP documentation 2024-05-09 15:35:50 -07:00
184b59ad1d Resolve merge conflicts 2024-05-09 12:51:24 -07:00
3113e40d0b Add mermaid diagrams to gcp auth docs 2024-05-08 20:09:08 -07:00
2406d3d904 Update GCP auth docs 2024-05-08 17:03:26 -07:00
e99182c141 Complete adding GCP GCE auth 2024-05-08 15:51:09 -07:00
522dd0836e feat: added validation for folder name duplicates 2024-05-08 23:25:33 +08:00
e461787c78 feat: added support for renaming folders in the overview page 2024-05-08 23:24:33 +08:00
ac469dbe4f Update GCP auth docs 2024-05-07 14:58:14 -07:00
d98430fe07 Merge remote-tracking branch 'origin' into gcp-iam-auth 2024-05-07 14:29:08 -07:00
82bafd02bb Fix merge conflicts 2024-05-07 14:28:41 -07:00
1d40d9e448 Begin frontend for GCP IAM Auth 2024-05-07 12:40:19 -07:00
e96ca8d355 Draft GCP IAM Auth docs 2024-05-07 12:15:18 -07:00
4d74d264dd Finish preliminary backend endpoints for GCP IAM Auth method 2024-05-07 10:42:39 -07:00
84 changed files with 3758 additions and 725 deletions

View File

@ -122,13 +122,13 @@ jobs:
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition - name: Download task definition
run: | run: |
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition - name: Render Amazon ECS task definition
id: render-web-container id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1 uses: aws-actions/amazon-ecs-render-task-definition@v1
with: with:
task-definition: task-definition.json task-definition: task-definition.json
container-name: infisical-prod-platform container-name: infisical-core-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }} image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info" environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service - name: Deploy to Amazon ECS service

View File

@ -34,11 +34,13 @@
"axios": "^1.6.7", "axios": "^1.6.7",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bullmq": "^5.3.3", "bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2", "cassandra-driver": "^4.7.2",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"fastify": "^4.26.0", "fastify": "^4.26.0",
"fastify-plugin": "^4.5.1", "fastify-plugin": "^4.5.1",
"google-auth-library": "^9.9.0",
"googleapis": "^137.1.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
@ -2938,6 +2940,7 @@
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "2.0.5", "@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9" "run-parallel": "^1.1.9"
@ -2950,6 +2953,7 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
} }
@ -2958,6 +2962,7 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"dependencies": { "dependencies": {
"@nodelib/fs.scandir": "2.1.5", "@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0" "fastq": "^1.6.0"
@ -6183,6 +6188,14 @@
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
}, },
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -6285,6 +6298,7 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
}, },
@ -6334,15 +6348,13 @@
} }
}, },
"node_modules/bullmq": { "node_modules/bullmq": {
"version": "5.3.3", "version": "5.4.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz",
"integrity": "sha512-Gc/68HxiCHLMPBiGIqtINxcf8HER/5wvBYMY/6x3tFejlvldUBFaAErMTLDv4TnPsTyzNPrfBKmFCEM58uVnJg==", "integrity": "sha512-dkR/KGUw18miLe3QWtvSlmGvEe08aZF+w1jZyqEHMWFW3RP4162qp6OGud0/QCAOjusiRI8UOxUhbnortPY+rA==",
"dependencies": { "dependencies": {
"cron-parser": "^4.6.0", "cron-parser": "^4.6.0",
"fast-glob": "^3.3.2",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimatch": "^9.0.3",
"msgpackr": "^1.10.1", "msgpackr": "^1.10.1",
"node-abort-controller": "^3.1.1", "node-abort-controller": "^3.1.1",
"semver": "^7.5.4", "semver": "^7.5.4",
@ -6350,28 +6362,6 @@
"uuid": "^9.0.0" "uuid": "^9.0.0"
} }
}, },
"node_modules/bullmq/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/bullmq/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/bundle-require": { "node_modules/bundle-require": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz",
@ -7759,6 +7749,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/extsprintf": { "node_modules/extsprintf": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
@ -7798,6 +7793,7 @@
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3", "@nodelib/fs.walk": "^1.2.3",
@ -7949,6 +7945,7 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -8286,6 +8283,88 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/gaxios": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.5.0.tgz",
"integrity": "sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w==",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gaxios/node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/gaxios/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/gaxios/node_modules/https-proxy-agent": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
"integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/gaxios/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gaxios/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/gcp-metadata": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
"dependencies": {
"gaxios": "^6.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/generate-function": { "node_modules/generate-function": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@ -8400,6 +8479,7 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": { "dependencies": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
}, },
@ -8482,6 +8562,69 @@
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true "dev": true
}, },
"node_modules/google-auth-library": {
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.9.0.tgz",
"integrity": "sha512-9l+zO07h1tDJdIHN74SpnWIlNR+OuOemXlWJlLP9pXy6vFtizgpEzMuwJa4lqY9UAdiAv5DVd5ql0Am916I+aA==",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-auth-library/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/google-auth-library/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/googleapis": {
"version": "137.1.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
"integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==",
"dependencies": {
"google-auth-library": "^9.0.0",
"googleapis-common": "^7.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/googleapis-common": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
"dependencies": {
"extend": "^3.0.2",
"gaxios": "^6.0.3",
"google-auth-library": "^9.7.0",
"qs": "^6.7.0",
"url-template": "^2.0.8",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -8504,6 +8647,37 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true "dev": true
}, },
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/gtoken/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/gtoken/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/handlebars": { "node_modules/handlebars": {
"version": "4.7.8", "version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@ -9000,6 +9174,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -9030,6 +9205,7 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@ -9064,6 +9240,7 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -9277,6 +9454,14 @@
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
}, },
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": { "node_modules/json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@ -9892,6 +10077,7 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
} }
@ -9908,6 +10094,7 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"dependencies": { "dependencies": {
"braces": "^3.0.2", "braces": "^3.0.2",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -9920,6 +10107,7 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -11549,6 +11737,7 @@
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -11901,6 +12090,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -12701,6 +12891,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -13703,6 +13894,11 @@
"querystring": "0.2.0" "querystring": "0.2.0"
} }
}, },
"node_modules/url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
},
"node_modules/url/node_modules/punycode": { "node_modules/url/node_modules/punycode": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",

View File

@ -95,11 +95,13 @@
"axios": "^1.6.7", "axios": "^1.6.7",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bullmq": "^5.3.3", "bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2", "cassandra-driver": "^4.7.2",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"fastify": "^4.26.0", "fastify": "^4.26.0",
"fastify-plugin": "^4.5.1", "fastify-plugin": "^4.5.1",
"google-auth-library": "^9.9.0",
"googleapis": "^137.1.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",

View File

@ -33,6 +33,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
@ -116,6 +117,7 @@ declare module "fastify" {
identityAccessToken: TIdentityAccessTokenServiceFactory; identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory; identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory; identityUa: TIdentityUaServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory;

View File

@ -62,6 +62,9 @@ import {
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAwsAuthsInsert, TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate, TIdentityAwsAuthsUpdate,
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
TIdentityOrgMemberships, TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate, TIdentityOrgMembershipsUpdate,
@ -228,6 +231,7 @@ import {
TWebhooksInsert, TWebhooksInsert,
TWebhooksUpdate TWebhooksUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
declare module "knex/types/tables" { declare module "knex/types/tables" {
interface Tables { interface Tables {
@ -301,6 +305,11 @@ declare module "knex/types/tables" {
>; >;
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>; [TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>; [TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
[TableName.SecretReference]: Knex.CompositeTableType<
TSecretReferences,
TSecretReferencesInsert,
TSecretReferencesUpdate
>;
[TableName.SecretBlindIndex]: Knex.CompositeTableType< [TableName.SecretBlindIndex]: Knex.CompositeTableType<
TSecretBlindIndexes, TSecretBlindIndexes,
TSecretBlindIndexesInsert, TSecretBlindIndexesInsert,
@ -329,6 +338,11 @@ declare module "knex/types/tables" {
TIdentityUniversalAuthsInsert, TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate TIdentityUniversalAuthsUpdate
>; >;
[TableName.IdentityGcpAuth]: Knex.CompositeTableType<
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate
>;
[TableName.IdentityAwsAuth]: Knex.CompositeTableType< [TableName.IdentityAwsAuth]: Knex.CompositeTableType<
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAwsAuthsInsert, TIdentityAwsAuthsInsert,

View File

@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityGcpAuth))) {
await knex.schema.createTable(TableName.IdentityGcpAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("type").notNullable();
t.string("allowedServiceAccounts").notNullable();
t.string("allowedProjects").notNullable();
t.string("allowedZones").notNullable(); // GCE only (fully qualified zone names)
});
}
await createOnUpdateTrigger(knex, TableName.IdentityGcpAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityGcpAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityGcpAuth);
}

View File

@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.SecretReference))) {
await knex.schema.createTable(TableName.SecretReference, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("environment").notNullable();
t.string("secretPath").notNullable();
t.uuid("secretId").notNullable();
t.foreign("secretId").references("id").inTable(TableName.Secret).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.SecretReference);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SecretReference);
await dropOnUpdateTrigger(knex, TableName.SecretReference);
}

View File

@ -0,0 +1,27 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityGcpAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
type: z.string(),
allowedServiceAccounts: z.string(),
allowedProjects: z.string(),
allowedZones: z.string()
});
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
export type TIdentityGcpAuthsInsert = Omit<z.input<typeof IdentityGcpAuthsSchema>, TImmutableDBKeys>;
export type TIdentityGcpAuthsUpdate = Partial<Omit<z.input<typeof IdentityGcpAuthsSchema>, TImmutableDBKeys>>;

View File

@ -18,6 +18,7 @@ export * from "./groups";
export * from "./identities"; export * from "./identities";
export * from "./identity-access-tokens"; export * from "./identity-access-tokens";
export * from "./identity-aws-auths"; export * from "./identity-aws-auths";
export * from "./identity-gcp-auths";
export * from "./identity-org-memberships"; export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege"; export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role"; export * from "./identity-project-membership-role";

View File

@ -28,6 +28,7 @@ export enum TableName {
ProjectUserMembershipRole = "project_user_membership_roles", ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys", ProjectKeys = "project_keys",
Secret = "secrets", Secret = "secrets",
SecretReference = "secret_references",
SecretBlindIndex = "secret_blind_indexes", SecretBlindIndex = "secret_blind_indexes",
SecretVersion = "secret_versions", SecretVersion = "secret_versions",
SecretFolder = "secret_folders", SecretFolder = "secret_folders",
@ -44,6 +45,7 @@ export enum TableName {
Identity = "identities", Identity = "identities",
IdentityAccessToken = "identity_access_tokens", IdentityAccessToken = "identity_access_tokens",
IdentityUniversalAuth = "identity_universal_auths", IdentityUniversalAuth = "identity_universal_auths",
IdentityGcpAuth = "identity_gcp_auths",
IdentityUaClientSecret = "identity_ua_client_secrets", IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths", IdentityAwsAuth = "identity_aws_auths",
IdentityOrgMembership = "identity_org_memberships", IdentityOrgMembership = "identity_org_memberships",
@ -144,5 +146,6 @@ export enum ProjectUpgradeStatus {
export enum IdentityAuthMethod { export enum IdentityAuthMethod {
Univeral = "universal-auth", Univeral = "universal-auth",
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth" AWS_AUTH = "aws-auth"
} }

View File

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const SecretReferencesSchema = z.object({
id: z.string().uuid(),
environment: z.string(),
secretPath: z.string(),
secretId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSecretReferences = z.infer<typeof SecretReferencesSchema>;
export type TSecretReferencesInsert = Omit<z.input<typeof SecretReferencesSchema>, TImmutableDBKeys>;
export type TSecretReferencesUpdate = Partial<Omit<z.input<typeof SecretReferencesSchema>, TImmutableDBKeys>>;

View File

@ -66,6 +66,10 @@ export enum EventType {
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth", LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth", ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth", UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
@ -410,6 +414,52 @@ interface RevokeIdentityUniversalAuthClientSecretEvent {
}; };
} }
interface LoginIdentityGcpAuthEvent {
type: EventType.LOGIN_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
identityGcpAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityGcpAuthEvent {
type: EventType.ADD_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
type: string;
allowedServiceAccounts: string;
allowedProjects: string;
allowedZones: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface UpdateIdentityGcpAuthEvent {
type: EventType.UPDATE_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
type?: string;
allowedServiceAccounts?: string;
allowedProjects?: string;
allowedZones?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityGcpAuthEvent {
type: EventType.GET_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityAwsAuthEvent { interface LoginIdentityAwsAuthEvent {
type: EventType.LOGIN_IDENTITY_AWS_AUTH; type: EventType.LOGIN_IDENTITY_AWS_AUTH;
metadata: { metadata: {
@ -708,6 +758,10 @@ export type Event =
| CreateIdentityUniversalAuthClientSecretEvent | CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent | GetIdentityUniversalAuthClientSecretsEvent
| RevokeIdentityUniversalAuthClientSecretEvent | RevokeIdentityUniversalAuthClientSecretEvent
| LoginIdentityGcpAuthEvent
| AddIdentityGcpAuthEvent
| UpdateIdentityGcpAuthEvent
| GetIdentityGcpAuthEvent
| LoginIdentityAwsAuthEvent | LoginIdentityAwsAuthEvent
| AddIdentityAwsAuthEvent | AddIdentityAwsAuthEvent
| UpdateIdentityAwsAuthEvent | UpdateIdentityAwsAuthEvent

View File

@ -7,12 +7,15 @@ import {
SecretType, SecretType,
TSecretApprovalRequestsSecretsInsert TSecretApprovalRequestsSecretsInsert
} from "@app/db/schemas"; } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn"; import { groupBy, pick, unique } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { getAllNestedSecretReferences } from "@app/services/secret/secret-fns";
import { TSecretQueueFactory } from "@app/services/secret/secret-queue"; import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
@ -53,6 +56,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretService: Pick< secretService: Pick<
TSecretServiceFactory, TSecretServiceFactory,
| "fnSecretBulkInsert" | "fnSecretBulkInsert"
@ -80,7 +84,8 @@ export const secretApprovalRequestServiceFactory = ({
snapshotService, snapshotService,
secretService, secretService,
secretVersionDAL, secretVersionDAL,
secretQueueService secretQueueService,
projectBotService
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -352,7 +357,7 @@ export const secretApprovalRequestServiceFactory = ({
} }
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Delete); const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === CommitType.Delete);
const botKey = await projectBotService.getBotKey(projectId).catch(() => null);
const mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => { const mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
const newSecrets = secretCreationCommits.length const newSecrets = secretCreationCommits.length
? await secretService.fnSecretBulkInsert({ ? await secretService.fnSecretBulkInsert({
@ -379,7 +384,17 @@ export const secretApprovalRequestServiceFactory = ({
]), ]),
tags: el?.tags.map(({ id }) => id), tags: el?.tags.map(({ id }) => id),
version: 1, version: 1,
type: SecretType.Shared type: SecretType.Shared,
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
})), })),
secretDAL, secretDAL,
secretVersionDAL, secretVersionDAL,
@ -414,7 +429,17 @@ export const secretApprovalRequestServiceFactory = ({
"secretReminderNote", "secretReminderNote",
"secretReminderRepeatDays", "secretReminderRepeatDays",
"secretBlindIndex" "secretBlindIndex"
]) ]),
references: botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
})
)
: undefined
} }
})), })),
secretDAL, secretDAL,

View File

@ -90,6 +90,7 @@ export const secretScanningServiceFactory = ({
const { const {
data: { repositories } data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation(); } = await octokit.apps.listReposAccessibleToInstallation();
if (!appCfg.DISABLE_SECRET_SCANNING) {
await Promise.all( await Promise.all(
repositories.map(({ id, full_name }) => repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({ secretScanningQueue.startFullRepoScan({
@ -99,6 +100,7 @@ export const secretScanningServiceFactory = ({
}) })
) )
); );
}
return { installatedApp }; return { installatedApp };
}; };
@ -151,6 +153,7 @@ export const secretScanningServiceFactory = ({
}; };
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => { const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
const appCfg = getConfig();
const { commits, repository, installation, pusher } = payload; const { commits, repository, installation, pusher } = payload;
if (!commits || !repository || !installation || !pusher) { if (!commits || !repository || !installation || !pusher) {
return; return;
@ -161,6 +164,7 @@ export const secretScanningServiceFactory = ({
}); });
if (!installationLink) return; if (!installationLink) return;
if (!appCfg.DISABLE_SECRET_SCANNING) {
await secretScanningQueue.startPushEventScan({ await secretScanningQueue.startPushEventScan({
commits, commits,
pusher: { name: pusher.name, email: pusher.email }, pusher: { name: pusher.name, email: pusher.email },
@ -168,6 +172,7 @@ export const secretScanningServiceFactory = ({
organizationId: installationLink.orgId, organizationId: installationLink.orgId,
installationId: String(installation?.id) installationId: String(installation?.id)
}); });
}
}; };
const handleRepoDeleteEvent = async (installationId: string, repositoryIds: string[]) => { const handleRepoDeleteEvent = async (installationId: string, repositoryIds: string[]) => {

View File

@ -252,6 +252,7 @@ export const FOLDERS = {
name: "The new name of the folder.", name: "The new name of the folder.",
path: "The path of the folder to update.", path: "The path of the folder to update.",
directory: "The new directory of the folder to update. (Deprecated in favor of path)", directory: "The new directory of the folder to update. (Deprecated in favor of path)",
projectSlug: "The slug of the project where the folder is located.",
workspaceId: "The ID of the project where the folder is located." workspaceId: "The ID of the project where the folder is located."
}, },
DELETE: { DELETE: {

View File

@ -13,6 +13,10 @@ const zodStrBool = z
const envSchema = z const envSchema = z
.object({ .object({
PORT: z.coerce.number().default(4000), PORT: z.coerce.number().default(4000),
DISABLE_SECRET_SCANNING: z
.enum(["true", "false"])
.default("false")
.transform((el) => el === "true"),
REDIS_URL: zpStr(z.string()), REDIS_URL: zpStr(z.string()),
HOST: zpStr(z.string().default("localhost")), HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(

View File

@ -65,7 +65,13 @@ export type TQueueJobTypes = {
}; };
[QueueName.IntegrationSync]: { [QueueName.IntegrationSync]: {
name: QueueJobs.IntegrationSync; name: QueueJobs.IntegrationSync;
payload: { projectId: string; environment: string; secretPath: string; depth?: number }; payload: {
projectId: string;
environment: string;
secretPath: string;
depth?: number;
deDupeQueue?: Record<string, boolean>;
};
}; };
[QueueName.SecretFullRepoScan]: { [QueueName.SecretFullRepoScan]: {
name: QueueJobs.SecretScan; name: QueueJobs.SecretScan;

View File

@ -5,8 +5,13 @@ import { getConfig } from "@app/lib/config/env";
export const maintenanceMode = fp(async (fastify) => { export const maintenanceMode = fp(async (fastify) => {
fastify.addHook("onRequest", async (req) => { fastify.addHook("onRequest", async (req) => {
const serverEnvs = getConfig(); const serverEnvs = getConfig();
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET" && serverEnvs.MAINTENANCE_MODE) { if (serverEnvs.MAINTENANCE_MODE) {
// skip if its universal auth login or renew
if (req.url === "/api/v1/auth/universal-auth/login" && req.method === "POST") return;
if (req.url === "/api/v1/auth/token/renew" && req.method === "POST") return;
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET") {
throw new Error("Infisical is in maintenance mode. Please try again later."); throw new Error("Infisical is in maintenance mode. Please try again later.");
} }
}
}); });
}); });

View File

@ -80,6 +80,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal"; import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@ -156,7 +158,10 @@ export const registerRoutes = async (
keyStore keyStore
}: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory } }: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
) => { ) => {
const appCfg = getConfig();
if (!appCfg.DISABLE_SECRET_SCANNING) {
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" }); await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
}
// db layers // db layers
const userDAL = userDALFactory(db); const userDAL = userDALFactory(db);
@ -205,6 +210,8 @@ export const registerRoutes = async (
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(db); const auditLogDAL = auditLogDALFactory(db);
const auditLogStreamDAL = auditLogStreamDALFactory(db); const auditLogStreamDAL = auditLogStreamDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db);
@ -538,8 +545,10 @@ export const registerRoutes = async (
folderDAL, folderDAL,
folderVersionDAL, folderVersionDAL,
projectEnvDAL, projectEnvDAL,
snapshotService snapshotService,
projectDAL
}); });
const integrationAuthService = integrationAuthServiceFactory({ const integrationAuthService = integrationAuthServiceFactory({
integrationAuthDAL, integrationAuthDAL,
integrationDAL, integrationDAL,
@ -598,6 +607,7 @@ export const registerRoutes = async (
}); });
const sarService = secretApprovalRequestServiceFactory({ const sarService = secretApprovalRequestServiceFactory({
permissionService, permissionService,
projectBotService,
folderDAL, folderDAL,
secretDAL, secretDAL,
secretTagDAL, secretTagDAL,
@ -702,6 +712,15 @@ export const registerRoutes = async (
identityUaDAL, identityUaDAL,
licenseService licenseService
}); });
const identityGcpAuthService = identityGcpAuthServiceFactory({
identityGcpAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
permissionService,
licenseService
});
const identityAwsAuthService = identityAwsAuthServiceFactory({ const identityAwsAuthService = identityAwsAuthServiceFactory({
identityAccessTokenDAL, identityAccessTokenDAL,
identityAwsAuthDAL, identityAwsAuthDAL,
@ -779,6 +798,7 @@ export const registerRoutes = async (
identityAccessToken: identityAccessTokenService, identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService, identityProject: identityProjectService,
identityUa: identityUaService, identityUa: identityUaService,
identityGcpAuth: identityGcpAuthService,
identityAwsAuth: identityAwsAuthService, identityAwsAuth: identityAwsAuthService,
secretApprovalPolicy: sapService, secretApprovalPolicy: sapService,
accessApprovalPolicy: accessApprovalPolicyService, accessApprovalPolicy: accessApprovalPolicyService,

View File

@ -20,16 +20,23 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).merge( config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
z.object({ isMigrationModeOn: z.boolean() }) isMigrationModeOn: z.boolean(),
) isSecretScanningDisabled: z.boolean()
})
}) })
} }
}, },
handler: async () => { handler: async () => {
const config = await getServerCfg(); const config = await getServerCfg();
const serverEnvs = getConfig(); const serverEnvs = getConfig();
return { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE } }; return {
config: {
...config,
isMigrationModeOn: serverEnvs.MAINTENANCE_MODE,
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING
}
};
} }
}); });

View File

@ -0,0 +1,268 @@
import { z } from "zod";
import { IdentityGcpAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateGcpAuthField } from "@app/services/identity-gcp-auth/identity-gcp-auth-validators";
export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/gcp-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
description: "Login with GCP Auth",
body: z.object({
identityId: z.string(),
jwt: z.string()
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityGcpAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityGcpAuthId: identityGcpAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/gcp-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach GCP Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
type: z.enum(["iam", "gce"]),
allowedServiceAccounts: validateGcpAuthField,
allowedProjects: validateGcpAuthField,
allowedZones: validateGcpAuthField,
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
accessTokenTTL: z
.number()
.int()
.min(1)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000),
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
}),
response: {
200: z.object({
identityGcpAuth: IdentityGcpAuthsSchema
})
}
},
handler: async (req) => {
const identityGcpAuth = await server.services.identityGcpAuth.attachGcpAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityGcpAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpAuth.identityId,
type: identityGcpAuth.type,
allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts,
allowedProjects: identityGcpAuth.allowedProjects,
allowedZones: identityGcpAuth.allowedZones,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
}
}
});
return { identityGcpAuth };
}
});
server.route({
method: "PATCH",
url: "/gcp-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update GCP Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
type: z.enum(["iam", "gce"]).optional(),
allowedServiceAccounts: validateGcpAuthField,
allowedProjects: validateGcpAuthField,
allowedZones: validateGcpAuthField,
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional(),
accessTokenTTL: z.number().int().min(0).optional(),
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
}),
response: {
200: z.object({
identityGcpAuth: IdentityGcpAuthsSchema
})
}
},
handler: async (req) => {
const identityGcpAuth = await server.services.identityGcpAuth.updateGcpAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityGcpAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpAuth.identityId,
type: identityGcpAuth.type,
allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts,
allowedProjects: identityGcpAuth.allowedProjects,
allowedZones: identityGcpAuth.allowedZones,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
}
}
});
return { identityGcpAuth };
}
});
server.route({
method: "GET",
url: "/gcp-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve GCP Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identityGcpAuth: IdentityGcpAuthsSchema
})
}
},
handler: async (req) => {
const identityGcpAuth = await server.services.identityGcpAuth.getGcpAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityGcpAuth.orgId,
event: {
type: EventType.GET_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpAuth.identityId
}
}
});
return { identityGcpAuth };
}
});
};

View File

@ -3,6 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
import { registerIdentityUaRouter } from "./identity-ua"; import { registerIdentityUaRouter } from "./identity-ua";
import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router";
@ -28,6 +29,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (authRouter) => { async (authRouter) => {
await authRouter.register(registerAuthRoutes); await authRouter.register(registerAuthRoutes);
await authRouter.register(registerIdentityUaRouter); await authRouter.register(registerIdentityUaRouter);
await authRouter.register(registerIdentityGcpAuthRouter);
await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAwsAuthRouter);
}, },

View File

@ -143,8 +143,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId) integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
}), }),
body: z.object({ body: z.object({
app: z.string().trim().describe(INTEGRATION.UPDATE.app), app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app),
appId: z.string().trim().describe(INTEGRATION.UPDATE.appId), appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId),
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive), isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
secretPath: z secretPath: z
.string() .string()
@ -154,7 +154,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.describe(INTEGRATION.UPDATE.secretPath), .describe(INTEGRATION.UPDATE.secretPath),
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment) environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
metadata: z
.object({
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
})
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@ -127,6 +127,70 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
} }
}); });
server.route({
url: "/batch",
method: "PATCH",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Update folders by batch",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectSlug: z.string().trim().describe(FOLDERS.UPDATE.projectSlug),
folders: z
.object({
id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path)
})
.array()
.min(1)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { newFolders, oldFolders, projectId } = await server.services.folder.updateManyFolders({
...req.body,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await Promise.all(
req.body.folders.map(async (folder, index) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.UPDATE_FOLDER,
metadata: {
environment: oldFolders[index].envId,
folderId: oldFolders[index].id,
folderPath: folder.path,
newFolderName: newFolders[index].name,
oldFolderName: oldFolders[index].name
}
}
});
})
);
return { folders: newFolders };
}
});
// TODO(daniel): Expose this route in api reference and write docs for it. // TODO(daniel): Expose this route in api reference and write docs for it.
server.route({ server.route({
method: "DELETE", method: "DELETE",

View File

@ -1926,4 +1926,41 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
return { secrets }; return { secrets };
} }
}); });
server.route({
method: "POST",
url: "/backfill-secret-references",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Backfill secret references",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId } = req.body;
const message = await server.services.secret.backfillSecretReferences({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId
});
return message;
}
});
}; };

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 TIdentityGcpAuthDALFactory = ReturnType<typeof identityGcpAuthDALFactory>;
export const identityGcpAuthDALFactory = (db: TDbClient) => {
const gcpAuthOrm = ormify(db, TableName.IdentityGcpAuth);
return gcpAuthOrm;
};

View File

@ -0,0 +1,70 @@
import axios from "axios";
import { OAuth2Client } from "google-auth-library";
import jwt from "jsonwebtoken";
import { UnauthorizedError } from "@app/lib/errors";
import { TDecodedGcpIamAuthJwt, TGcpIdTokenPayload } from "./identity-gcp-auth-types";
/**
* Validates that the identity token [jwt] sent in from a client GCE instance as part of GCP ID Token authentication
* is valid.
* @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience).
* @param {string} jwt - The identity token to validate.
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
*/
export const validateIdTokenIdentity = async ({
identityId,
jwt: identityToken
}: {
identityId: string;
jwt: string;
}) => {
const oAuth2Client = new OAuth2Client();
const response = await oAuth2Client.getFederatedSignonCerts();
const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
identityToken,
response.certs,
identityId, // audience
["https://accounts.google.com"]
);
const payload = ticket.getPayload() as TGcpIdTokenPayload;
if (!payload || !payload.email) throw new UnauthorizedError();
return { email: payload.email, computeEngineDetails: payload.google?.compute_engine };
};
/**
* Validates that the signed JWT token for a GCP service account is valid as part of GCP IAM authentication.
* @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience).
* @param {string} jwt - The signed JWT token to validate.
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
* @returns
*/
export const validateIamIdentity = async ({
identityId,
jwt: serviceAccountJwt
}: {
identityId: string;
jwt: string;
}) => {
const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt;
const { sub, aud } = decodedJwt.payload;
const {
data
}: {
data: {
[key: string]: string;
};
} = await axios.get(`https://www.googleapis.com/service_accounts/v1/metadata/x509/${sub}`);
const publicKey = data[decodedJwt.header.kid];
jwt.verify(serviceAccountJwt, publicKey, {
algorithms: ["RS256"]
});
if (aud !== identityId) throw new UnauthorizedError();
return { email: sub };
};

View File

@ -0,0 +1,324 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal";
import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns";
import {
TAttachGcpAuthDTO,
TGcpIdentityDetails,
TGetGcpAuthDTO,
TLoginGcpAuthDTO,
TUpdateGcpAuthDTO
} from "./identity-gcp-auth-types";
type TIdentityGcpAuthServiceFactoryDep = {
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityGcpAuthServiceFactory = ReturnType<typeof identityGcpAuthServiceFactory>;
export const identityGcpAuthServiceFactory = ({
identityGcpAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
permissionService,
licenseService
}: TIdentityGcpAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (!identityGcpAuth) throw new UnauthorizedError();
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpAuth.identityId });
if (!identityMembershipOrg) throw new UnauthorizedError();
let gcpIdentityDetails: TGcpIdentityDetails;
switch (identityGcpAuth.type) {
case "gce": {
gcpIdentityDetails = await validateIdTokenIdentity({
identityId,
jwt: gcpJwt
});
break;
}
case "iam": {
gcpIdentityDetails = await validateIamIdentity({
identityId,
jwt: gcpJwt
});
break;
}
default: {
throw new BadRequestError({ message: "Invalid GCP Auth type" });
}
}
if (identityGcpAuth.allowedServiceAccounts) {
// validate if the service account is in the list of allowed service accounts
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
.split(",")
.map((serviceAccount) => serviceAccount.trim())
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
if (!isServiceAccountAllowed) throw new UnauthorizedError();
}
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) {
// validate if the project that the service account belongs to is in the list of allowed projects
const isProjectAllowed = identityGcpAuth.allowedProjects
.split(",")
.map((project) => project.trim())
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
if (!isProjectAllowed) throw new UnauthorizedError();
}
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
const isZoneAllowed = identityGcpAuth.allowedZones
.split(",")
.map((zone) => zone.trim())
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
if (!isZoneAllowed) throw new UnauthorizedError();
}
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityGcpAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityGcpAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg };
};
const attachGcpAuth = async ({
identityId,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add GCP Auth to already configured identity"
});
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const identityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
const doc = await identityGcpAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.GCP_AUTH
},
tx
);
return doc;
});
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
};
const updateGcpAuth = async ({
identityId,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
throw new BadRequestError({
message: "Failed to update GCP Auth"
});
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityGcpAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updatedGcpAuth = await identityGcpAuthDAL.updateById(identityGcpAuth.id, {
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return {
...updatedGcpAuth,
orgId: identityMembershipOrg.orgId
};
};
const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
throw new BadRequestError({
message: "The identity does not have GCP Auth attached"
});
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
};
return {
login,
attachGcpAuth,
updateGcpAuth,
getGcpAuth
};
};

View File

@ -0,0 +1,78 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginGcpAuthDTO = {
identityId: string;
jwt: string;
};
export type TAttachGcpAuthDTO = {
identityId: string;
type: "iam" | "gce";
allowedServiceAccounts: string;
allowedProjects: string;
allowedZones: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateGcpAuthDTO = {
identityId: string;
type?: "iam" | "gce";
allowedServiceAccounts?: string;
allowedProjects?: string;
allowedZones?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetGcpAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
type TComputeEngineDetails = {
instance_creation_timestamp: number;
instance_id: string;
instance_name: string;
project_id: string;
project_number: number;
zone: string;
};
export type TGcpIdentityDetails = {
email: string;
computeEngineDetails?: TComputeEngineDetails;
};
export type TGcpIdTokenPayload = {
aud: string;
azp: string;
email: string;
email_verified: boolean;
exp: number;
google?: {
compute_engine: TComputeEngineDetails;
};
iat: number;
iss: string;
sub: string;
};
export type TDecodedGcpIamAuthJwt = {
header: {
alg: string;
kid: string;
typ: string;
};
payload: {
sub: string;
aud: string;
};
signature: string;
metadata: {
[key: string]: string;
};
};

View File

@ -0,0 +1,14 @@
import { z } from "zod";
export const validateGcpAuthField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
// Trim each ID and join with ', ' to ensure formatting
return data
.split(",")
.map((id) => id.trim())
.join(", ");
});

View File

@ -82,6 +82,7 @@ export const identityProjectServiceFactory = ({
role, role,
project.id project.id
); );
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge) if (!hasPriviledge)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@ -135,16 +136,18 @@ export const identityProjectServiceFactory = ({
message: `Identity with id ${identityId} doesn't exists in project with id ${projectId}` message: `Identity with id ${identityId} doesn't exists in project with id ${projectId}`
}); });
const { permission: identityRolePermission } = await permissionService.getProjectPermission( for await (const { role: requestedRoleChange } of roles) {
ActorType.IDENTITY, const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
projectIdentity.identityId, requestedRoleChange,
projectIdentity.projectId, projectId
actorAuthMethod,
actorOrgId
); );
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges) const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
}
}
// validate custom roles input // validate custom roles input
const customInputRoles = roles.filter( const customInputRoles = roles.filter(

View File

@ -9,9 +9,12 @@
import { import {
CreateSecretCommand, CreateSecretCommand,
DescribeSecretCommand,
GetSecretValueCommand, GetSecretValueCommand,
ResourceNotFoundException, ResourceNotFoundException,
SecretsManagerClient, SecretsManagerClient,
TagResourceCommand,
UntagResourceCommand,
UpdateSecretCommand UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager"; } from "@aws-sdk/client-secrets-manager";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
@ -574,6 +577,7 @@ const syncSecretsAWSSecretManager = async ({
if (awsSecretManagerSecret?.SecretString) { if (awsSecretManagerSecret?.SecretString) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
} }
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) { if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) {
await secretsManager.send( await secretsManager.send(
new UpdateSecretCommand({ new UpdateSecretCommand({
@ -582,7 +586,88 @@ const syncSecretsAWSSecretManager = async ({
}) })
); );
} }
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
if (secretAWSTag && secretAWSTag.length) {
const describedSecret = await secretsManager.send(
// requires secretsmanager:DescribeSecret policy
new DescribeSecretCommand({
SecretId: integration.app as string
})
);
if (!describedSecret.Tags) return;
const integrationTagObj = secretAWSTag.reduce(
(acc, item) => {
acc[item.key] = item.value;
return acc;
},
{} as Record<string, string>
);
const awsTagObj = (describedSecret.Tags || []).reduce(
(acc, item) => {
if (item.Key && item.Value) {
acc[item.Key] = item.Value;
}
return acc;
},
{} as Record<string, string>
);
const tagsToUpdate: { Key: string; Value: string }[] = [];
const tagsToDelete: { Key: string; Value: string }[] = [];
describedSecret.Tags?.forEach((tag) => {
if (tag.Key && tag.Value) {
if (!(tag.Key in integrationTagObj)) {
// delete tag from AWS secret manager
tagsToDelete.push({
Key: tag.Key,
Value: tag.Value
});
} else if (tag.Value !== integrationTagObj[tag.Key]) {
// update tag in AWS secret manager
tagsToUpdate.push({
Key: tag.Key,
Value: integrationTagObj[tag.Key]
});
}
}
});
secretAWSTag?.forEach((tag) => {
if (!(tag.key in awsTagObj)) {
// create tag in AWS secret manager
tagsToUpdate.push({
Key: tag.key,
Value: tag.value
});
}
});
if (tagsToUpdate.length) {
await secretsManager.send(
new TagResourceCommand({
SecretId: integration.app as string,
Tags: tagsToUpdate
})
);
}
if (tagsToDelete.length) {
await secretsManager.send(
new UntagResourceCommand({
SecretId: integration.app as string,
TagKeys: tagsToDelete.map((tag) => tag.Key)
})
);
}
}
} catch (err) { } catch (err) {
// case when AWS manager can't find the specified secret
if (err instanceof ResourceNotFoundException && secretsManager) { if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send( await secretsManager.send(
new CreateSecretCommand({ new CreateSecretCommand({

View File

@ -103,7 +103,8 @@ export const integrationServiceFactory = ({
owner, owner,
isActive, isActive,
environment, environment,
secretPath secretPath,
metadata
}: TUpdateIntegrationDTO) => { }: TUpdateIntegrationDTO) => {
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" }); if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
@ -127,7 +128,17 @@ export const integrationServiceFactory = ({
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
secretPath secretPath,
metadata: {
...(integration.metadata as object),
...metadata
}
});
await secretQueueService.syncIntegrations({
environment: folder.environment.slug,
secretPath,
projectId: folder.projectId
}); });
return updatedIntegration; return updatedIntegration;

View File

@ -33,13 +33,27 @@ export type TCreateIntegrationDTO = {
export type TUpdateIntegrationDTO = { export type TUpdateIntegrationDTO = {
id: string; id: string;
app: string; app?: string;
appId: string; appId?: string;
isActive?: boolean; isActive?: boolean;
secretPath: string; secretPath: string;
targetEnvironment: string; targetEnvironment: string;
owner: string; owner: string;
environment: string; environment: string;
metadata?: {
secretPrefix?: string;
secretSuffix?: string;
secretGCPLabel?: {
labelName: string;
labelValue: string;
};
secretAWSTag?: {
key: string;
value: string;
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
};
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteIntegrationDTO = { export type TDeleteIntegrationDTO = {

View File

@ -8,9 +8,16 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal"; import { TSecretFolderDALFactory } from "./secret-folder-dal";
import { TCreateFolderDTO, TDeleteFolderDTO, TGetFolderDTO, TUpdateFolderDTO } from "./secret-folder-types"; import {
TCreateFolderDTO,
TDeleteFolderDTO,
TGetFolderDTO,
TUpdateFolderDTO,
TUpdateManyFoldersDTO
} from "./secret-folder-types";
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
type TSecretFolderServiceFactoryDep = { type TSecretFolderServiceFactoryDep = {
@ -19,6 +26,7 @@ type TSecretFolderServiceFactoryDep = {
folderDAL: TSecretFolderDALFactory; folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderVersionDAL: TSecretFolderVersionDALFactory; folderVersionDAL: TSecretFolderVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
}; };
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>; export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
@ -28,7 +36,8 @@ export const secretFolderServiceFactory = ({
snapshotService, snapshotService,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
folderVersionDAL folderVersionDAL,
projectDAL
}: TSecretFolderServiceFactoryDep) => { }: TSecretFolderServiceFactoryDep) => {
const createFolder = async ({ const createFolder = async ({
projectId, projectId,
@ -116,6 +125,105 @@ export const secretFolderServiceFactory = ({
return folder; return folder;
}; };
const updateManyFolders = async ({
actor,
actorId,
projectSlug,
actorAuthMethod,
actorOrgId,
folders
}: TUpdateManyFoldersDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) {
throw new BadRequestError({ message: "Project not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
});
const result = await folderDAL.transaction(async (tx) =>
Promise.all(
folders.map(async (newFolder) => {
const { environment, path: secretPath, id, name } = newFolder;
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!parentFolder) {
throw new BadRequestError({ message: "Secret path not found", name: "Batch update folder" });
}
const env = await projectEnvDAL.findOne({ projectId: project.id, slug: environment });
if (!env) {
throw new BadRequestError({ message: "Environment not found", name: "Batch update folder" });
}
const folder = await folderDAL
.findOne({ envId: env.id, id, parentId: parentFolder.id })
// now folder api accepts id based change
// this is for cli backward compatiability and when cli removes this, we will remove this logic
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
if (name !== folder.name) {
// ensure that new folder name is unique
const folderToCheck = await folderDAL.findOne({
name,
envId: env.id,
parentId: parentFolder.id
});
if (folderToCheck) {
throw new BadRequestError({
message: "Folder with specified name already exists",
name: "Batch update folder"
});
}
}
const [doc] = await folderDAL.update(
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
{ name },
tx
);
await folderVersionDAL.create(
{
name: doc.name,
envId: doc.envId,
version: doc.version,
folderId: doc.id
},
tx
);
if (!doc) {
throw new BadRequestError({ message: "Folder not found", name: "Batch update folder" });
}
return { oldFolder: folder, newFolder: doc };
})
)
);
await Promise.all(result.map(async (res) => snapshotService.performSnapshot(res.newFolder.parentId as string)));
return {
projectId: project.id,
newFolders: result.map((res) => res.newFolder),
oldFolders: result.map((res) => res.oldFolder)
};
};
const updateFolder = async ({ const updateFolder = async ({
projectId, projectId,
actor, actor,
@ -151,6 +259,21 @@ export const secretFolderServiceFactory = ({
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id })); .catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new BadRequestError({ message: "Folder not found" });
if (name !== folder.name) {
// ensure that new folder name is unique
const folderToCheck = await folderDAL.findOne({
name,
envId: env.id,
parentId: parentFolder.id
});
if (folderToCheck) {
throw new BadRequestError({
message: "Folder with specified name already exists",
name: "Update folder"
});
}
}
const newFolder = await folderDAL.transaction(async (tx) => { const newFolder = await folderDAL.transaction(async (tx) => {
const [doc] = await folderDAL.update({ envId: env.id, id: folder.id, parentId: parentFolder.id }, { name }, tx); const [doc] = await folderDAL.update({ envId: env.id, id: folder.id, parentId: parentFolder.id }, { name }, tx);
@ -239,6 +362,7 @@ export const secretFolderServiceFactory = ({
return { return {
createFolder, createFolder,
updateFolder, updateFolder,
updateManyFolders,
deleteFolder, deleteFolder,
getFolders getFolders
}; };

View File

@ -13,6 +13,16 @@ export type TUpdateFolderDTO = {
name: string; name: string;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateManyFoldersDTO = {
projectSlug: string;
folders: {
environment: string;
path: string;
id: string;
name: string;
}[];
} & Omit<TProjectPermission, "projectId">;
export type TDeleteFolderDTO = { export type TDeleteFolderDTO = {
environment: string; environment: string;
path: string; path: string;

View File

@ -243,6 +243,74 @@ export const secretDALFactory = (db: TDbClient) => {
} }
}; };
const upsertSecretReferences = async (
data: {
secretId: string;
references: Array<{ environment: string; secretPath: string }>;
}[] = [],
tx?: Knex
) => {
try {
if (!data.length) return;
await (tx || db)(TableName.SecretReference)
.whereIn(
"secretId",
data.map(({ secretId }) => secretId)
)
.delete();
const newSecretReferences = data
.filter(({ references }) => references.length)
.flatMap(({ secretId, references }) =>
references.map(({ environment, secretPath }) => ({
secretPath,
secretId,
environment
}))
);
if (!newSecretReferences.length) return;
const secretReferences = await (tx || db)(TableName.SecretReference).insert(newSecretReferences);
return secretReferences;
} catch (error) {
throw new DatabaseError({ error, name: "UpsertSecretReference" });
}
};
const findReferencedSecretReferences = async (projectId: string, envSlug: string, secretPath: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretReference)
.where({
secretPath,
environment: envSlug
})
.join(TableName.Secret, `${TableName.Secret}.id`, `${TableName.SecretReference}.secretId`)
.join(TableName.SecretFolder, `${TableName.Secret}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where("projectId", projectId)
.select(selectAllTableCols(TableName.SecretReference))
.select("folderId");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindReferencedSecretReferences" });
}
};
// special query to backfill secret value
const findAllProjectSecretValues = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.Secret)
.join(TableName.SecretFolder, `${TableName.Secret}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where("projectId", projectId)
// not empty
.whereNotNull("secretValueCiphertext")
.select("secretValueTag", "secretValueCiphertext", "secretValueIV", `${TableName.Secret}.id` as "id");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindAllProjectSecretValues" });
}
};
return { return {
...secretOrm, ...secretOrm,
update, update,
@ -252,6 +320,9 @@ export const secretDALFactory = (db: TDbClient) => {
getSecretTags, getSecretTags,
findByFolderId, findByFolderId,
findByFolderIds, findByFolderIds,
findByBlindIndexes findByBlindIndexes,
upsertSecretReferences,
findReferencedSecretReferences,
findAllProjectSecretValues
}; };
}; };

View File

@ -194,6 +194,7 @@ type TInterpolateSecretArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
}; };
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => { export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const fetchSecretsCrossEnv = () => { const fetchSecretsCrossEnv = () => {
const fetchCache: Record<string, Record<string, string>> = {}; const fetchCache: Record<string, Record<string, string>> = {};
@ -235,7 +236,6 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
}; };
}; };
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
const recursivelyExpandSecret = async ( const recursivelyExpandSecret = async (
expandedSec: Record<string, string>, expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>, interpolatedSec: Record<string, string>,
@ -396,6 +396,37 @@ export const decryptSecretRaw = (
}; };
}; };
/**
* Grabs and processes nested secret references from a string
*
* This function looks for patterns that match the interpolation syntax in the input string.
* It filters out references that include nested paths, splits them into environment and
* secret path parts, and then returns an array of objects with the environment and the
* joined secret path.
*
* @param {string} maybeSecretReference - The string that has the potential secret references.
* @returns {Array<{ environment: string, secretPath: string }>} - An array of objects
* with the environment and joined secret path.
*
* @example
* const value = "Hello ${dev.someFolder.OtherFolder.SECRET_NAME} and ${prod.anotherFolder.SECRET_NAME}";
* const result = getAllNestedSecretReferences(value);
* // result will be:
* // [
* // { environment: 'dev', secretPath: '/someFolder/OtherFolder' },
* // { environment: 'prod', secretPath: '/anotherFolder' }
* // ]
*/
export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
return references
.filter((el) => el.includes("."))
.map((el) => {
const [environment, ...secretPathList] = el.split(".");
return { environment, secretPath: path.join("/", ...secretPathList.slice(0, -1)) };
});
};
/** /**
* Checks and handles secrets using a blind index method. * Checks and handles secrets using a blind index method.
* The function generates mappings between secret names and their blind indexes, validates user IDs for personal secrets, and retrieves secrets from the database based on their blind indexes. * The function generates mappings between secret names and their blind indexes, validates user IDs for personal secrets, and retrieves secrets from the database based on their blind indexes.
@ -467,7 +498,7 @@ export const fnSecretBulkInsert = async ({
tx tx
}: TFnSecretBulkInsert) => { }: TFnSecretBulkInsert) => {
const newSecrets = await secretDAL.insertMany( const newSecrets = await secretDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })), inputSecrets.map(({ tags, references, ...el }) => ({ ...el, folderId })),
tx tx
); );
const newSecretGroupByBlindIndex = groupBy(newSecrets, (item) => item.secretBlindIndex as string); const newSecretGroupByBlindIndex = groupBy(newSecrets, (item) => item.secretBlindIndex as string);
@ -478,13 +509,19 @@ export const fnSecretBulkInsert = async ({
})) }))
); );
const secretVersions = await secretVersionDAL.insertMany( const secretVersions = await secretVersionDAL.insertMany(
inputSecrets.map(({ tags, ...el }) => ({ inputSecrets.map(({ tags, references, ...el }) => ({
...el, ...el,
folderId, folderId,
secretId: newSecretGroupByBlindIndex[el.secretBlindIndex as string][0].id secretId: newSecretGroupByBlindIndex[el.secretBlindIndex as string][0].id
})), })),
tx tx
); );
await secretDAL.upsertSecretReferences(
inputSecrets.map(({ references = [], secretBlindIndex }) => ({
secretId: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id,
references
}))
);
if (newSecretTags.length) { if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx); const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
@ -509,7 +546,7 @@ export const fnSecretBulkUpdate = async ({
secretVersionTagDAL secretVersionTagDAL
}: TFnSecretBulkUpdate) => { }: TFnSecretBulkUpdate) => {
const newSecrets = await secretDAL.bulkUpdate( const newSecrets = await secretDAL.bulkUpdate(
inputSecrets.map(({ filter, data: { tags, ...data } }) => ({ inputSecrets.map(({ filter, data: { tags, references, ...data } }) => ({
filter: { ...filter, folderId }, filter: { ...filter, folderId },
data data
})), })),
@ -522,6 +559,14 @@ export const fnSecretBulkUpdate = async ({
})), })),
tx tx
); );
await secretDAL.upsertSecretReferences(
inputSecrets
.filter(({ data: { references } }) => Boolean(references))
.map(({ data: { references = [] } }, i) => ({
secretId: newSecrets[i].id,
references
}))
);
const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) => const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) =>
tags !== undefined ? { tags, secretId: newSecrets[i].id } : [] tags !== undefined ? { tags, secretId: newSecrets[i].id } : []
); );
@ -591,32 +636,16 @@ export const createManySecretsRawFnFactory = ({
folderId, folderId,
isNew: true, isNew: true,
blindIndexCfg, blindIndexCfg,
userId,
secretDAL secretDAL
}); });
const inputSecrets = await Promise.all( const inputSecrets = secrets.map((secret) => {
secrets.map(async (secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretReferences = getAllNestedSecretReferences(secret.secretValue || "");
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to create personal secret override for no corresponding shared secret"
});
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return { return {
type: secret.type, type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null, userId: secret.type === SecretType.Personal ? userId : null,
@ -631,10 +660,15 @@ export const createManySecretsRawFnFactory = ({
secretCommentIV: secretCommentEncrypted.iv, secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag, secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags tags: secret.tags,
references: secretReferences
}; };
}) });
);
// get all tags
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tags.length !== tagIds.length) throw new BadRequestError({ message: "Tag not found" });
const newSecrets = await secretDAL.transaction(async (tx) => const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({ fnSecretBulkInsert({
@ -703,37 +737,16 @@ export const updateManySecretsRawFnFactory = ({
userId userId
}); });
const inputSecrets = await Promise.all( const inputSecrets = secrets.map((secret) => {
secrets.map(async (secret) => {
if (secret.newSecretName === "") { if (secret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" }); throw new BadRequestError({ message: "New secret name cannot be empty" });
} }
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretReferences = getAllNestedSecretReferences(secret.secretValue || "");
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to update personal secret override for no corresponding shared secret"
});
if (secret.newSecretName)
throw new BadRequestError({ message: "Personal secret cannot change the key name" });
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return { return {
type: secret.type, type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null, userId: secret.type === SecretType.Personal ? userId : null,
@ -749,10 +762,10 @@ export const updateManySecretsRawFnFactory = ({
secretCommentIV: secretCommentEncrypted.iv, secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag, secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags tags: secret.tags,
references: secretReferences
}; };
}) });
);
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags); const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : []; const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];

View File

@ -59,6 +59,7 @@ export type TGetSecrets = {
}; };
const MAX_SYNC_SECRET_DEPTH = 5; const MAX_SYNC_SECRET_DEPTH = 5;
const uniqueIntegrationKey = (environment: string, secretPath: string) => `integration-${environment}-${secretPath}`;
export const secretQueueFactory = ({ export const secretQueueFactory = ({
queueService, queueService,
@ -102,28 +103,35 @@ export const secretQueueFactory = ({
folderDAL folderDAL
}); });
const syncIntegrations = async (dto: TGetSecrets) => { const syncIntegrations = async (dto: TGetSecrets & { deDupeQueue?: Record<string, boolean> }) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 5, attempts: 3,
delay: 1000, delay: 1000,
backoff: { backoff: {
type: "exponential", type: "exponential",
delay: 3000 delay: 3000
}, },
removeOnComplete: true, removeOnComplete: true,
removeOnFail: { removeOnFail: true
count: 5 // keep the most recent jobs
}
}); });
}; };
const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => { const syncSecrets = async ({
deDupeQueue = {},
...dto
}: TGetSecrets & { depth?: number; deDupeQueue?: Record<string, boolean> }) => {
const deDuplicationKey = uniqueIntegrationKey(dto.environment, dto.secretPath);
if (deDupeQueue?.[deDuplicationKey]) {
return;
}
// eslint-disable-next-line
deDupeQueue[deDuplicationKey] = true;
logger.info( logger.info(
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]` `syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]`
); );
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, { await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`, jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
removeOnFail: { count: 5 }, removeOnFail: true,
removeOnComplete: true, removeOnComplete: true,
delay: 1000, delay: 1000,
attempts: 5, attempts: 5,
@ -132,7 +140,7 @@ export const secretQueueFactory = ({
delay: 3000 delay: 3000
} }
}); });
await syncIntegrations(dto); await syncIntegrations({ ...dto, deDupeQueue });
}; };
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
@ -326,7 +334,7 @@ export const secretQueueFactory = ({
}; };
queueService.start(QueueName.IntegrationSync, async (job) => { queueService.start(QueueName.IntegrationSync, async (job) => {
const { environment, projectId, secretPath, depth = 1 } = job.data; const { environment, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data;
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) { if (!folder) {
@ -349,21 +357,68 @@ export const secretQueueFactory = ({
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId); const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds); const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id); const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id);
await Promise.all(
imports
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
.map(({ folderId }) => {
const syncDto = {
depth: depth + 1,
projectId,
secretPath: foldersGroupedById[folderId][0].path,
environment: foldersGroupedById[folderId][0].environmentSlug
};
logger.info( logger.info(
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]` `getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
); );
return syncSecrets(syncDto); await Promise.all(
imports
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueIntegrationKey(
foldersGroupedById[folderId][0].environmentSlug,
foldersGroupedById[folderId][0].path
)
]
)
.map(({ folderId }) =>
syncSecrets({
depth: depth + 1,
projectId,
secretPath: foldersGroupedById[folderId][0].path,
environment: foldersGroupedById[folderId][0].environmentSlug,
deDupeQueue
}) })
)
);
}
const secretReferences = await secretDAL.findReferencedSecretReferences(
projectId,
folder.environment.slug,
secretPath
);
if (secretReferences.length) {
const referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
const referencedFoldersGroupedById = groupBy(referencedFolders, (i) => i.child || i.id);
logger.info(
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
secretReferences
.filter(({ folderId }) => Boolean(referencedFoldersGroupedById[folderId][0].path))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueIntegrationKey(
referencedFoldersGroupedById[folderId][0].environmentSlug,
referencedFoldersGroupedById[folderId][0].path
)
]
)
.map(({ folderId }) =>
syncSecrets({
depth: depth + 1,
projectId,
secretPath: referencedFoldersGroupedById[folderId][0].path,
environment: referencedFoldersGroupedById[folderId][0].environmentSlug,
deDupeQueue
})
)
); );
} }
} else { } else {

View File

@ -2,12 +2,22 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas"; import {
ProjectMembershipRole,
SecretEncryptionAlgo,
SecretKeyEncoding,
SecretsSchema,
SecretType
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { buildSecretBlindIndexFromName, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import {
buildSecretBlindIndexFromName,
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8
} from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy, pick } from "@app/lib/fn"; import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@ -27,12 +37,14 @@ import {
fnSecretBlindIndexCheck, fnSecretBlindIndexCheck,
fnSecretBulkInsert, fnSecretBulkInsert,
fnSecretBulkUpdate, fnSecretBulkUpdate,
getAllNestedSecretReferences,
interpolateSecrets, interpolateSecrets,
recursivelyGetSecretPaths recursivelyGetSecretPaths
} from "./secret-fns"; } from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue"; import { TSecretQueueFactory } from "./secret-queue";
import { import {
TAttachSecretTagsDTO, TAttachSecretTagsDTO,
TBackFillSecretReferencesDTO,
TCreateBulkSecretDTO, TCreateBulkSecretDTO,
TCreateManySecretRawDTO, TCreateManySecretRawDTO,
TCreateSecretDTO, TCreateSecretDTO,
@ -91,6 +103,22 @@ export const secretServiceFactory = ({
secretImportDAL, secretImportDAL,
secretVersionTagDAL secretVersionTagDAL
}: TSecretServiceFactoryDep) => { }: TSecretServiceFactoryDep) => {
const getSecretReference = async (projectId: string) => {
// if bot key missing means e2e still exist
const botKey = await projectBotService.getBotKey(projectId).catch(() => null);
return (el: { ciphertext?: string; iv: string; tag: string }) =>
botKey
? getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.ciphertext || "",
iv: el.iv,
tag: el.tag,
key: botKey
})
)
: undefined;
};
// utility function to get secret blind index data // utility function to get secret blind index data
const interalGenSecBlindIndexByName = async (projectId: string, secretName: string) => { const interalGenSecBlindIndexByName = async (projectId: string, secretName: string) => {
const appCfg = getConfig(); const appCfg = getConfig();
@ -225,6 +253,7 @@ export const secretServiceFactory = ({
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const { secretName, type, ...el } = inputSecret; const { secretName, type, ...el } = inputSecret;
const references = await getSecretReference(projectId);
const secret = await secretDAL.transaction((tx) => const secret = await secretDAL.transaction((tx) =>
fnSecretBulkInsert({ fnSecretBulkInsert({
folderId, folderId,
@ -237,7 +266,12 @@ export const secretServiceFactory = ({
userId: inputSecret.type === SecretType.Personal ? actorId : null, userId: inputSecret.type === SecretType.Personal ? actorId : null,
algorithm: SecretEncryptionAlgo.AES_256_GCM, algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8, keyEncoding: SecretKeyEncoding.UTF8,
tags: inputSecret.tags tags: inputSecret.tags,
references: references({
ciphertext: inputSecret.secretValueCiphertext,
iv: inputSecret.secretValueIV,
tag: inputSecret.secretValueTag
})
} }
], ],
secretDAL, secretDAL,
@ -335,6 +369,7 @@ export const secretServiceFactory = ({
const { secretName, ...el } = inputSecret; const { secretName, ...el } = inputSecret;
const references = await getSecretReference(projectId);
const updatedSecret = await secretDAL.transaction(async (tx) => const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({ fnSecretBulkUpdate({
folderId, folderId,
@ -360,7 +395,12 @@ export const secretServiceFactory = ({
"secretReminderRepeatDays", "secretReminderRepeatDays",
"tags" "tags"
]), ]),
secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName] secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName],
references: references({
ciphertext: inputSecret.secretValueCiphertext,
iv: inputSecret.secretValueIV,
tag: inputSecret.secretValueTag
})
} }
} }
], ],
@ -700,6 +740,7 @@ export const secretServiceFactory = ({
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : []; const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tags.length !== tagIds.length) throw new BadRequestError({ message: "Tag not found" }); if (tags.length !== tagIds.length) throw new BadRequestError({ message: "Tag not found" });
const references = await getSecretReference(projectId);
const newSecrets = await secretDAL.transaction(async (tx) => const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({ fnSecretBulkInsert({
inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({ inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({
@ -708,7 +749,12 @@ export const secretServiceFactory = ({
secretBlindIndex: keyName2BlindIndex[secretName], secretBlindIndex: keyName2BlindIndex[secretName],
type: SecretType.Shared, type: SecretType.Shared,
algorithm: SecretEncryptionAlgo.AES_256_GCM, algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8 keyEncoding: SecretKeyEncoding.UTF8,
references: references({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag
})
})), })),
folderId, folderId,
secretDAL, secretDAL,
@ -783,6 +829,8 @@ export const secretServiceFactory = ({
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags); const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : []; const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const references = await getSecretReference(projectId);
const secrets = await secretDAL.transaction(async (tx) => const secrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({ fnSecretBulkUpdate({
folderId, folderId,
@ -799,7 +847,15 @@ export const secretServiceFactory = ({
? newKeyName2BlindIndex[newSecretName] ? newKeyName2BlindIndex[newSecretName]
: keyName2BlindIndex[secretName], : keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM, algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8 keyEncoding: SecretKeyEncoding.UTF8,
references:
el.secretValueIV && el.secretValueTag
? references({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag
})
: undefined
} }
})), })),
secretDAL, secretDAL,
@ -1488,6 +1544,51 @@ export const secretServiceFactory = ({
}; };
}; };
// this is a backfilling API for secret references
// what it does is it will go through all the secret values and parse all references
// populate the secret reference to do sync integrations
const backfillSecretReferences = async ({
projectId,
actor,
actorId,
actorOrgId,
actorAuthMethod
}: TBackFillSecretReferencesDTO) => {
const { hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
if (!hasRole(ProjectMembershipRole.Admin))
throw new BadRequestError({ message: "Only admins are allowed to take this action" });
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey)
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
await secretDAL.transaction(async (tx) => {
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
await secretDAL.upsertSecretReferences(
secrets.map(({ id, secretValueCiphertext, secretValueIV, secretValueTag }) => ({
secretId: id,
references: getAllNestedSecretReferences(
decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
key: botKey
})
)
}))
);
});
return { message: "Successfully backfilled secret references" };
};
return { return {
attachTags, attachTags,
detachTags, detachTags,
@ -1508,6 +1609,7 @@ export const secretServiceFactory = ({
updateManySecretsRaw, updateManySecretsRaw,
deleteManySecretsRaw, deleteManySecretsRaw,
getSecretVersions, getSecretVersions,
backfillSecretReferences,
// external services function // external services function
fnSecretBulkDelete, fnSecretBulkDelete,
fnSecretBulkUpdate, fnSecretBulkUpdate,

View File

@ -223,11 +223,13 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
secretId: string; secretId: string;
}; };
export type TSecretReference = { environment: string; secretPath: string };
export type TFnSecretBulkInsert = { export type TFnSecretBulkInsert = {
folderId: string; folderId: string;
tx?: Knex; tx?: Knex;
inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[] }>; inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[]; references?: TSecretReference[] }>;
secretDAL: Pick<TSecretDALFactory, "insertMany">; secretDAL: Pick<TSecretDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
@ -236,8 +238,11 @@ export type TFnSecretBulkInsert = {
export type TFnSecretBulkUpdate = { export type TFnSecretBulkUpdate = {
folderId: string; folderId: string;
projectId: string; projectId: string;
inputSecrets: { filter: Partial<TSecrets>; data: TSecretsUpdate & { tags?: string[] } }[]; inputSecrets: {
secretDAL: Pick<TSecretDALFactory, "bulkUpdate">; filter: Partial<TSecrets>;
data: TSecretsUpdate & { tags?: string[]; references?: TSecretReference[] };
}[];
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "deleteTagsManySecret">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "deleteTagsManySecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
@ -294,6 +299,8 @@ export type TRemoveSecretReminderDTO = {
repeatDays: number; repeatDays: number;
}; };
export type TBackFillSecretReferencesDTO = TProjectPermission;
// --- // ---
export type TCreateManySecretsRawFnFactory = { export type TCreateManySecretsRawFnFactory = {

View File

@ -27,7 +27,7 @@ sequenceDiagram
Note over Infis: Step 4: Identity Property Validation Note over Infis: Step 4: Identity Property Validation
Infis->>Client: Return short-lived access token Infis->>Client: Return short-lived access token
Note over Client,Infis: Step 4: Access Infisical API with Token Note over Client,Infis: Step 5: Access Infisical API with Token
Client->>Infis: Make authenticated requests using the short-lived access token Client->>Infis: Make authenticated requests using the short-lived access token
``` ```
@ -36,9 +36,13 @@ sequenceDiagram
At a high-level, Infisical authenticates an IAM principal by verifying its identity and checking that it meets specific requirements (e.g. it is an allowed IAM principal ARN) at the `/api/v1/auth/aws-auth/login` endpoint. If successful, At a high-level, Infisical authenticates an IAM principal by verifying its identity and checking that it meets specific requirements (e.g. it is an allowed IAM principal ARN) at the `/api/v1/auth/aws-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
In AWS Auth, an IAM principal signs a `GetCallerIdentity` query using the [AWS Signature v4 algorithm](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html); this is done using the credentials from the AWS environment where the IAM principal is running. To be more specific:
The query data including the request method, request body, and request headers are sent to Infisical afterwhich Infisical forwards the signed query to AWS STS API via the [sts:GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) method to verify and obtain the identity of the IAM principal.
Once obtained, the identity information is verified against specified requirements such as if the associated IAM principal ARN is allowed to authenticate with Infisical. If all is well, Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. 1. The client IAM principal signs a `GetCallerIdentity` query using the [AWS Signature v4 algorithm](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html); this is done using the credentials from the AWS environment where the IAM principal is running.
2. The client sends the signed query data to Infisical including the request method, request body, and request headers.
3. Infisical reconstructs the query and sends it to AWS STS API via the [sts:GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) method for verification and obtains the identity associated with the IAM principal.
4. Infisical checks the identity's properties against set criteria such **Allowed Principal ARNs**.
5. If all is well, Infisical returns a short-lived access token that the IAM principal can use to make authenticated requests to the Infisical API.
<Note> <Note>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent We recommend using one of Infisical's clients like SDKs or the Infisical Agent
@ -51,7 +55,7 @@ as part of the AWS Auth workflow.
</Note> </Note>
## Workflow ## Guide
In the following steps, we explore how to create and use identities for your workloads and applications on AWS to In the following steps, we explore how to create and use identities for your workloads and applications on AWS to
access the Infisical API using the AWS Auth authentication method. access the Infisical API using the AWS Auth authentication method.

View File

@ -0,0 +1,351 @@
---
title: GCP Auth
description: "Learn how to authenticate with Infisical for services on Google Cloud Platform"
---
**GCP Auth** is a GCP-native authentication method for GCP resources to access Infisical. It consists of two sub-methods/approaches:
- GCP ID Token Auth: For GCP services including [Compute Engine](https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature), [App Engine standard environment](https://cloud.google.com/appengine/docs/standard/python3/runtime#metadata_server), [App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/python/runtime#metadata_server), [Cloud Functions](https://cloud.google.com/functions/docs/securing/function-identity#using_the_metadata_server_to_acquire_tokens), [Cloud Run](https://cloud.google.com/run/docs/container-contract#metadata-server), [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity#instance_metadata), and [Cloud Build](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity#instance_metadata) to authenticate with Infisical.
- GCP IAM Auth: For Google Cloud Platform (GCP) service accounts to authenticate with Infisical.
<Tabs>
<Tab title="Google ID Token Auth">
## Diagram
The following sequence digram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical.
```mermaid
sequenceDiagram
participant GCE as GCP Service
participant Infis as Infisical
participant Google as OAuth2 API
Note over GCE,Google: Step 1: Instance Identity Token Retrieval
GCE->>Google: Request instance identity metadata token
Google-->>GCE: Return JWT token with RS256 signature
Note over GCE,Infis: Step 2: Identity Token Login Operation
GCE->>Infis: Send JWT token to /api/v1/auth/gcp-auth/login
Infis->>Google: Request OAuth2 certificates
Google-->>Infis: Return certificates
Note over Infis: Step 3: Identity Token Verification
Note over Infis: Step 4: Identity Property Validation
Infis->>GCE: Return short-lived access token
Note over GCE,Infis: Step 4: Access Infisical API with Token
GCE->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates a GCP resource by verifying its identity and checking that it meets specific requirements (e.g. it is an allowed GCE instance) at the `/api/v1/auth/gcp-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. The client running on a GCP service obtains an [ID token](https://cloud.google.com/docs/authentication/get-id-token) constituting the identity for a GCP resource such as a GCE instance or Cloud Function; this is a unique JWT token that includes details about the instance as well as Google's [RS256 signature](https://datatracker.ietf.org/doc/html/rfc7518#section-3.3).
2. The client sends the ID token to Infisical.
3. Infisical verifies the token against Google's [public OAuth2 certificates](https://www.googleapis.com/oauth2/v3/certs).
4. Infisical checks if the entity behind the ID token is allowed to authenticate with Infisical based on set criteria such as **Allowed Service Account Emails**.
5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
<Note>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
to authenticate with Infisical using GCP ID Token Auth as they handle the
authentication process including generating the instance ID token for you.
Also, note that Infisical needs network-level access to send requests to the Google Cloud API
as part of the GCP Auth workflow.
</Note>
## Guide
In the following steps, we explore how to create and use identities for your workloads and applications on GCP to
access the Infisical API using the GCP ID Token authentication method.
<Steps>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **GCP Auth** and set the **Type** to **GCP ID Token Auth**.
![identities create gcp auth method](/images/platform/identities/identities-org-create-gcp-gce-auth-method.png)
Here's some more guidance on each field:
- Allowed Service Account Emails: A comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical; this could be something like `test@project.iam.gserviceaccount.com`, `12345-compute@developer.gserviceaccount.com`, etc.
- Allowed Projects: A comma-separated list of trusted GCP projects that the GCE instance must belong to authenticate with Infisical. Note that this validation property will only work for GCE instances.
- Allowed Zones: A comma-separated list of trusted zones that the GCE instances must belong to authenticate with Infisical; this should be the fully-qualified zone name in the format `<region>-<zone>`like `us-central1-a`, `us-west1-b`, etc. Note that this validation property will only work for GCE instances.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you need to generate an [ID token](https://cloud.google.com/docs/authentication/get-id-token) constituting the identity of the present GCE instance and make a request to the `/api/v1/auth/gcp-auth/login` endpoint containing the token in exchange for an access token.
We provide a few code examples below of how you can authenticate with Infisical to access the [Infisical API](/api-reference/overview/introduction).
<AccordionGroup>
<Accordion
title="Sample code for generating the ID token"
>
Start by making a request from the GCE instance to obtain the ID token.
For more examples of how to obtain the token in Java, Go, Node.js, etc. refer to the [official documentation](https://cloud.google.com/docs/authentication/get-id-token#curl).
#### Sample request
<CodeGroup>
```bash curl
curl -H "Metadata-Flavor: Google" \
'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=<identityId>'
```
</CodeGroup>
<Note>
Note that you should replace `<identityId>` with the ID of the identity you created in step 1.
</Note>
Next use send the obtained JWT token along to authenticate with Infisical and obtain an access token.
#### Sample request
```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/auth/gcp-auth/login' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'identityId=...' \
--data-urlencode 'jwt=...'
```
#### Sample response
```bash Response
{
"accessToken": "...",
"expiresIn": 7200,
"accessTokenMaxTTL": 43244
"tokenType": "Bearer"
}
```
Next, you can use the access token to access the [Infisical API](/api-reference/overview/introduction)
</Accordion>
</AccordionGroup>
<Tip>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
</Note>
</Step>
</Steps>
</Tab>
<Tab title="GCP IAM Auth">
## Diagram
The following sequence digram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical.
```mermaid
sequenceDiagram
participant GCE as Client
participant Infis as Infisical
participant Google as Cloud IAM
Note over GCE,Google: Step 1: Signed JWT Token Generation
GCE->>Google: Request to generate signed JWT token
Google-->>GCE: Return signed JWT token
Note over GCE,Infis: Step 2: JWT Token Login Operation
GCE->>Infis: Send signed JWT token to /api/v1/auth/gcp-auth/login
Infis->>Google: Request public key
Google-->>Infis: Return public key
Note over Infis: Step 3: JWT Token Verification
Note over Infis: Step 4: JWT Property Validation
Infis->>GCE: Return short-lived access token
Note over GCE,Infis: Step 5: Access Infisical API with Token
GCE->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates an IAM service account by verifying its identity and checking that it meets specific requirements (e.g. it is an allowed service account) at the `/api/v1/auth/gcp-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. The client generates a signed JWT token using the `projects.serviceAccounts.signJwt` [API method](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt); this is done using the service account credentials associated with the client.
2. The client sends the signed JWT token to Infisical.
3. Infisical verifies the signed JWT token.
4. Infisical checks if the service account behind the JWT token is allowed to authenticate with Infisical based **Allowed Service Account Emails**.
5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
<Note>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
to authenticate with Infisical using GCP IAM Auth as they handle the
authentication process including generating the signed JWT token.
Also, note that Infisical needs network-level access to send requests to the Google Cloud API
as part of the GCP Auth workflow.
</Note>
## Guide
In the following steps, we explore how to create and use identities for your workloads and applications on GCP to
access the Infisical API using the GCP IAM authentication method.
<Steps>
<Step title="Creating an identity">
To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **GCP IAM Auth** and set the **Type** to **GCP IAM Auth**.
![identities create gcp auth method](/images/platform/identities/identities-org-create-gcp-iam-auth-method.png)
Here's some more guidance on each field:
- Allowed Service Account Emails: A comma-separated list of trusted IAM service account emails that are allowed to authenticate with Infisical; this could be something like `test@project.iam.gserviceaccount.com`, `12345-compute@developer.gserviceaccount.com`, etc.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time.
- Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses.
- Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you need to generate a signed JWT token using the `projects.serviceAccounts.signJwt` [API method](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt) and make a request to the `/api/v1/auth/gcp-auth/login` endpoint containing the signed JWT token in exchange for an access token.
<Info>
Make sure that the service account has the `iam.serviceAccounts.signJwt` permission or the `roles/iam.serviceAccountTokenCreator` role.
</Info>
We provide a few code examples below of how you can authenticate with Infisical to access the [Infisical API](/api-reference/overview/introduction).
<AccordionGroup>
<Accordion
title="Sample code for generating a signed JWT token"
>
The following code provides a generic example of how you can generate a signed JWT token against the `projects.serviceAccounts.signJwt` API method.
The shown example uses Node.js and the official [google-auth-library](https://github.com/googleapis/google-auth-library-nodejs#readme) package but you can use any language you wish.
```javascript
const { GoogleAuth } = require("google-auth-library");
const auth = new GoogleAuth({
scopes: "https://www.googleapis.com/auth/cloud-platform",
});
const credentials = await auth.getCredentials();
const identityId = "<your-infisical-identity-id>";
const jwtPayload = {
sub: credentials.client_email,
aud: identityId,
};
const { data } = await client.request({
url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${credentials.client_email}:signJwt`,
method: "POST",
data: { payload: JSON.stringify(jwtPayload) },
});
const jwt = data.signedJwt // send this jwt to Infisical in the next step
```
#### Sample request
```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/auth/gcp-auth/login' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'identityId=...' \
--data-urlencode 'jwt=...'
```
#### Sample response
```bash Response
{
"accessToken": "...",
"expiresIn": 7200,
"accessTokenMaxTTL": 43244
"tokenType": "Bearer"
}
```
Next, you can use the access token to access the [Infisical API](/api-reference/overview/introduction)
</Accordion>
</AccordionGroup>
<Tip>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
</Note>
</Step>
</Steps>
</Tab>
</Tabs>

View File

@ -7,7 +7,7 @@ description: "Learn how to use Machine Identities to programmatically interact w
An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP). An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP).
Each identity must authenticate using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) or [AWS Auth](/documentation/platform/identities/aws-auth) to get back a short-lived access token to be used in subsequent requests. Each identity must authenticate with the Infisical API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth), [AWS Auth](/documentation/platform/identities/aws-auth), or [GCP Auth](/documentation/platform/identities/gcp-auth) to get back a short-lived access token to be used in subsequent requests.
![organization identities](/images/platform/organization/organization-machine-identities.png) ![organization identities](/images/platform/organization/organization-machine-identities.png)
@ -21,7 +21,7 @@ Key Features:
A typical workflow for using identities consists of four steps: A typical workflow for using identities consists of four steps:
1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities. 1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth) or [AWS Auth](/documentation/platform/identities/aws-auth). This step also involves configuring an authentication method for it.
2. Adding the identity to the project(s) you want it to have access to. 2. Adding the identity to the project(s) you want it to have access to.
3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back. 3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
4. Authenticating subsequent requests with the Infisical API using the short-lived access token. 4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
@ -39,6 +39,9 @@ To interact with various resources in Infisical, Machine Identities are able to
- [Universal Auth](/documentation/platform/identities/universal-auth): A platform-agnostic authentication method that can be configured on an identity suitable to authenticate from any platform/environment. - [Universal Auth](/documentation/platform/identities/universal-auth): A platform-agnostic authentication method that can be configured on an identity suitable to authenticate from any platform/environment.
- [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for IAM principals like EC2 instances or Lambda functions to authenticate with Infisical. - [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for IAM principals like EC2 instances or Lambda functions to authenticate with Infisical.
- [GCP Auth](/documentation/platform/identities/gcp-auth): A GCP-native authentication method for GCP resources (e.g. Compute Engine, App Engine, Cloud Run, Google Kubernetes Engine, IAM service accounts, etc.) to authenticate with Infisical.
IAM service accounts and GCE instances to authenticate with Infisical.
## FAQ ## FAQ

View File

@ -26,10 +26,14 @@ sequenceDiagram
## Concept ## Concept
In this method, Infisical authenticates an identity by verifying the credentials issued for it at the `/api/v1/auth/universal-auth/login` endpoint. If successful, In this method, Infisical authenticates a client by verifying the credentials issued for it at the `/api/v1/auth/universal-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
In Universal Auth, an identity is given a **Client ID** and one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for a short-lived access token to authenticate with the Infisical API. To be more specific:
1. The client submits a **Client ID** and **Client Secret** to Infisical.
2. Infisical verifies the credential pair.
3. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
## Guide ## Guide
@ -148,7 +152,7 @@ using the Universal Auth authentication method.
- The client secret/access token is being used from an untrusted IP. - The client secret/access token is being used from an untrusted IP.
</Accordion> </Accordion>
<Accordion title="What is access token renewal and TTL/Max TTL?"> <Accordion title="What is access token renewal and TTL/Max TTL?">
A identity access token can have a time-to-live (TTL) or incremental lifetime afterwhich it expires. A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires.
In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter.

View File

@ -3,7 +3,7 @@ title: "Secret Versioning"
description: "Learn how secret versioning works in Infisical." description: "Learn how secret versioning works in Infisical."
--- ---
Every time a secret change is persformed, a new version of the same secret is created. Every time a secret change is performed, a new version of the same secret is created.
Such versions can be accessed visually by opening up the [secret sidebar](/documentation/platform/project#drawer) (as seen below) or [retrieved via API](/api-reference/endpoints/secrets/read) Such versions can be accessed visually by opening up the [secret sidebar](/documentation/platform/project#drawer) (as seen below) or [retrieved via API](/api-reference/endpoints/secrets/read)
by specifying the `version` query parameter. by specifying the `version` query parameter.

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

View File

@ -29,7 +29,9 @@ Prerequisites:
"secretsmanager:GetSecretValue", "secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret", "secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret", "secretsmanager:UpdateSecret",
"secretsmanager:DescribeSecret", // if you need to add tags to secrets
"secretsmanager:TagResource", // if you need to add tags to secrets "secretsmanager:TagResource", // if you need to add tags to secrets
"secretsmanager:UntagResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key "kms:ListKeys", // if you need to specify the KMS key
"kms:ListAliases", // if you need to specify the KMS key "kms:ListAliases", // if you need to specify the KMS key
"kms:Encrypt", // if you need to specify the KMS key "kms:Encrypt", // if you need to specify the KMS key

View File

@ -153,6 +153,7 @@
"documentation/platform/auth-methods/email-password", "documentation/platform/auth-methods/email-password",
"documentation/platform/token", "documentation/platform/token",
"documentation/platform/identities/universal-auth", "documentation/platform/identities/universal-auth",
"documentation/platform/identities/gcp-auth",
"documentation/platform/identities/aws-auth", "documentation/platform/identities/aws-auth",
"documentation/platform/mfa", "documentation/platform/mfa",
{ {

View File

@ -176,7 +176,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
![infisical-selfhost](/images/self-hosting/applicable-to-all/selfhost-signup.png) ![infisical-selfhost](/images/self-hosting/applicable-to-all/selfhost-signup.png)
</Step> </Step>
<Step title="Upgrade your instance"> <Step title="Upgrade your instance">
To upgrade your instance of Infisical simply update the docker image tag in your Halm values and rerun the command below. To upgrade your instance of Infisical simply update the docker image tag in your Helm values and rerun the command below.
```bash ```bash
helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml

View File

@ -120,7 +120,7 @@ export default function NavHeader({
passHref passHref
legacyBehavior legacyBehavior
href={{ href={{
pathname: "/project/[id]/secrets/v2/[env]", pathname: "/project/[id]/secrets/[env]",
query: { id: router.query.id, env: router.query.env } query: { id: router.query.id, env: router.query.env }
}} }}
> >

View File

@ -0,0 +1,19 @@
import { forwardRef, HTMLAttributes } from "react";
type Props = {
symbolName: string;
} & HTMLAttributes<HTMLDivElement>;
export const FontAwesomeSymbol = forwardRef<HTMLDivElement, Props>(
({ symbolName, ...props }, ref) => {
return (
<div ref={ref} {...props}>
<svg className="w-inherit h-inherit">
<use href={`#${symbolName}`} />
</svg>
</div>
);
}
);
FontAwesomeSymbol.displayName = "FontAwesomeSymbol";

View File

@ -0,0 +1 @@
export { FontAwesomeSymbol } from "./FontAwesomeSymbol";

View File

@ -1,17 +1,42 @@
import { TextareaHTMLAttributes, useEffect, useRef, useState } from "react"; import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { twMerge } from "tailwind-merge";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useDebounce } from "@app/hooks"; import { useDebounce, useToggle } from "@app/hooks";
import { useGetFoldersByEnv, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api"; import { useGetProjectFolders, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
import { SecretInput } from "../SecretInput"; import { SecretInput } from "../SecretInput";
const REGEX_UNCLOSED_SECRET_REFERENCE = /\${(?![^{}]*\})/g; const getIndexOfUnclosedRefToTheLeft = (value: string, pos: number) => {
const REGEX_OPEN_SECRET_REFERENCE = /\${/g; // take substring up to pos in order to consider edits for closed references
for (let i = pos; i >= 1; i -= 1) {
if (value[i] === "}") return -1;
if (value[i - 1] === "$" && value[i] === "{") {
return i;
}
}
return -1;
};
const getIndexOfUnclosedRefToTheRight = (value: string, pos: number) => {
// use it with above to identify an open ${
for (let i = pos; i < value.length; i += 1) {
if (value[i] === "}") return i - 1;
}
return -1;
};
const getClosingSymbol = (isSelectedSecret: boolean, isClosed: boolean) => {
if (!isClosed) {
return isSelectedSecret ? "}" : ".";
}
if (!isSelectedSecret) return ".";
return "";
};
const mod = (n: number, m: number) => ((n % m) + m) % m;
export enum ReferenceType { export enum ReferenceType {
ENVIRONMENT = "environment", ENVIRONMENT = "environment",
@ -19,8 +44,9 @@ export enum ReferenceType {
SECRET = "secret" SECRET = "secret"
} }
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & { type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "value"> & {
value?: string | null; value?: string;
onChange: (val: string) => void;
isImport?: boolean; isImport?: boolean;
isVisible?: boolean; isVisible?: boolean;
isReadOnly?: boolean; isReadOnly?: boolean;
@ -31,295 +57,248 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
}; };
type ReferenceItem = { type ReferenceItem = {
name: string; label: string;
type: ReferenceType; type: ReferenceType;
slug?: string; slug: string;
}; };
export const InfisicalSecretInput = ({ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
value: propValue, (
{
value = "",
onChange,
containerClassName, containerClassName,
secretPath: propSecretPath, secretPath: propSecretPath,
environment: propEnvironment, environment: propEnvironment,
onChange,
...props ...props
}: Props) => { },
const [inputValue, setInputValue] = useState(propValue ?? ""); ref
const [isSuggestionsOpen, setIsSuggestionsOpen] = useState(false); ) => {
const [currentCursorPosition, setCurrentCursorPosition] = useState(0);
const [currentReference, setCurrentReference] = useState<string>("");
const [secretPath, setSecretPath] = useState<string>(propSecretPath || "/");
const [environment, setEnvironment] = useState<string | undefined>(propEnvironment);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId); const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const debouncedValue = useDebounce(value, 500);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const inputRef = useRef<HTMLTextAreaElement>(null);
const popoverContentRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useToggle(false);
const currentCursorPosition = inputRef.current?.selectionStart || 0;
const suggestionSource = useMemo(() => {
const left = getIndexOfUnclosedRefToTheLeft(debouncedValue, currentCursorPosition - 1);
if (left === -1) return { left, value: "", predicate: "", isDeep: false };
const suggestionSourceValue = debouncedValue.slice(left + 1, currentCursorPosition);
let suggestionSourceEnv: string | undefined = propEnvironment;
let suggestionSourceSecretPath: string | undefined = propSecretPath || "/";
// means its like <environment>.<folder1>.<...more folder>.secret
const isDeep = suggestionSourceValue.includes(".");
let predicate = suggestionSourceValue;
if (isDeep) {
const [envSlug, ...folderPaths] = suggestionSourceValue.split(".");
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
suggestionSourceEnv = isValidEnvSlug ? envSlug : undefined;
suggestionSourceSecretPath = `/${folderPaths.slice(0, -1)?.join("/")}`;
predicate = folderPaths[folderPaths.length - 1];
}
return {
left: left + 1,
// the full value inside a ${<value>}
value: suggestionSourceValue,
// the final part after staging.dev.<folder1>.<predicate>
predicate,
isOpen: left !== -1,
isDeep,
environment: suggestionSourceEnv,
secretPath: suggestionSourceSecretPath
};
}, [debouncedValue]);
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
const { data: secrets } = useGetProjectSecrets({ const { data: secrets } = useGetProjectSecrets({
decryptFileKey: decryptFileKey!, decryptFileKey: decryptFileKey!,
environment: environment || currentWorkspace?.environments?.[0].slug!, environment: suggestionSource.environment || "",
secretPath, secretPath: suggestionSource.secretPath || "",
workspaceId workspaceId,
options: {
enabled: isPopupOpen
}
}); });
const { folderNames: folders } = useGetFoldersByEnv({ const { data: folders } = useGetProjectFolders({
path: secretPath, environment: suggestionSource.environment || "",
environments: [environment || currentWorkspace?.environments?.[0].slug!], path: suggestionSource.secretPath || "",
projectId: workspaceId projectId: workspaceId,
options: {
enabled: isPopupOpen
}
}); });
const debouncedCurrentReference = useDebounce(currentReference, 100); const suggestions = useMemo(() => {
if (!isPopupOpen) return [];
// reset highlight whenever recomputation happens
setHighlightedIndex(-1);
const suggestionsArr: ReferenceItem[] = [];
const predicate = suggestionSource.predicate.toLowerCase();
const [listReference, setListReference] = useState<ReferenceItem[]>([]); if (!suggestionSource.isDeep) {
const [highlightedIndex, setHighlightedIndex] = useState(-1); // At first level only environments and secrets
const inputRef = useRef<HTMLTextAreaElement>(null); (currentWorkspace?.environments || []).forEach(({ name, slug }) => {
const isPopupOpen = isSuggestionsOpen && listReference.length > 0 && currentReference.length > 0; if (name.toLowerCase().startsWith(predicate))
suggestionsArr.push({
useEffect(() => { label: name,
setInputValue(propValue ?? ""); slug,
}, [propValue]);
useEffect(() => {
let currentEnvironment = propEnvironment;
let currentSecretPath = propSecretPath || "/";
if (!currentReference) {
setSecretPath(currentSecretPath);
setEnvironment(currentEnvironment);
return;
}
const isNested = currentReference.includes(".");
if (isNested) {
const [envSlug, ...folderPaths] = currentReference.split(".");
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
currentEnvironment = isValidEnvSlug ? envSlug : undefined;
// should be based on the last valid section (with .)
folderPaths.pop();
currentSecretPath = `/${folderPaths?.join("/")}`;
}
setSecretPath(currentSecretPath);
setEnvironment(currentEnvironment);
}, [debouncedCurrentReference]);
useEffect(() => {
const currentListReference: ReferenceItem[] = [];
const isNested = currentReference?.includes(".");
if (!currentReference) {
setListReference(currentListReference);
return;
}
if (!environment) {
currentWorkspace?.environments.forEach((env) => {
currentListReference.unshift({
name: env.slug,
type: ReferenceType.ENVIRONMENT type: ReferenceType.ENVIRONMENT
}); });
}); });
} else if (isNested) { } else {
folders?.forEach((folder) => { // one deeper levels its based on an environment folders and secrets
currentListReference.unshift({ name: folder, type: ReferenceType.FOLDER }); (folders || []).forEach(({ name }) => {
}); if (name.toLowerCase().startsWith(predicate))
} else if (environment) { suggestionsArr.push({
currentWorkspace?.environments.forEach((env) => { label: name,
currentListReference.unshift({ slug: name,
name: env.slug, type: ReferenceType.FOLDER
type: ReferenceType.ENVIRONMENT
}); });
}); });
} }
(secrets || []).forEach(({ key }) => {
secrets?.forEach((secret) => { if (key.toLowerCase().startsWith(predicate))
currentListReference.unshift({ name: secret.key, type: ReferenceType.SECRET }); suggestionsArr.push({
label: key,
slug: key,
type: ReferenceType.SECRET
}); });
// Get fragment inside currentReference
const searchFragment = isNested ? currentReference.split(".").pop() || "" : currentReference;
const filteredListRef = currentListReference
.filter((suggestionEntry) =>
suggestionEntry.name.toUpperCase().startsWith(searchFragment.toUpperCase())
)
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
setListReference(filteredListRef);
}, [secrets, environment, debouncedCurrentReference]);
const getIndexOfUnclosedRefToTheLeft = (pos: number) => {
// take substring up to pos in order to consider edits for closed references
const unclosedReferenceIndexMatches = [
...inputValue.substring(0, pos).matchAll(REGEX_UNCLOSED_SECRET_REFERENCE)
].map((match) => match.index);
// find unclosed reference index less than the current cursor position
let indexIter = -1;
unclosedReferenceIndexMatches.forEach((index) => {
if (index !== undefined && index > indexIter && index < pos) {
indexIter = index;
}
}); });
return suggestionsArr;
}, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
return indexIter; const handleSuggestionSelect = (selectIndex?: number) => {
}; const selectedSuggestion =
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
const getIndexOfUnclosedRefToTheRight = (pos: number) => {
const unclosedReferenceIndexMatches = [...inputValue.matchAll(REGEX_OPEN_SECRET_REFERENCE)].map(
(match) => match.index
);
// find the next unclosed reference index to the right of the current cursor position
// this is so that we know the limitation for slicing references
let indexIter = Infinity;
unclosedReferenceIndexMatches.forEach((index) => {
if (index !== undefined && index > pos && index < indexIter) {
indexIter = index;
}
});
return indexIter;
};
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// open suggestions if current position is to the right of an unclosed secret reference
const indexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
if (indexIter === -1) {
return;
}
setIsSuggestionsOpen(true);
if (e.key !== "Enter") {
// current reference is then going to be based on the text from the closest ${ to the right
// until the current cursor position
const openReferenceValue = inputValue.slice(indexIter + 2, currentCursorPosition);
setCurrentReference(openReferenceValue);
}
};
const handleSuggestionSelect = (selectedIndex?: number) => {
const selectedSuggestion = listReference[selectedIndex ?? highlightedIndex];
if (!selectedSuggestion) { if (!selectedSuggestion) {
return; return;
} }
const leftIndexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition); const rightBracketIndex = getIndexOfUnclosedRefToTheRight(value, suggestionSource.left);
const rightIndexLimit = getIndexOfUnclosedRefToTheRight(currentCursorPosition); const isEnclosed = rightBracketIndex !== -1;
// <lhsValue>${}<rhsvalue>
if (leftIndexIter === -1) { const lhsValue = value.slice(0, suggestionSource.left);
return; const rhsValue = value.slice(
} rightBracketIndex !== -1 ? rightBracketIndex + 1 : currentCursorPosition
);
let newValue = ""; // mid will be computed value inside the interpolation
const currentOpenRef = inputValue.slice(leftIndexIter + 2, currentCursorPosition); const mid = suggestionSource.isDeep
if (currentOpenRef.includes(".")) { ? `${suggestionSource.value.slice(0, -suggestionSource.predicate.length || undefined)}${selectedSuggestion.slug
// append suggestion after last DOT (.) }`
const lastDotIndex = currentReference.lastIndexOf("."); : selectedSuggestion.slug;
const existingPath = currentReference.slice(0, lastDotIndex); // whether we should append . or closing bracket on selecting suggestion
const refEndAfterAppending = Math.min( const closingSymbol = getClosingSymbol(
leftIndexIter + selectedSuggestion.type === ReferenceType.SECRET,
3 + isEnclosed
existingPath.length +
selectedSuggestion.name.length +
Number(selectedSuggestion.type !== ReferenceType.SECRET),
rightIndexLimit - 1
); );
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${existingPath}.${ const newValue = `${lhsValue}${mid}${closingSymbol}${rhsValue}`;
selectedSuggestion.name onChange?.(newValue);
}${selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"}${inputValue.slice( // this delay is for cursor adjustment
refEndAfterAppending // cannot do this without a delay because what happens in onChange gets propogated after the cursor change
)}`; // Thus the cursor goes last to avoid that we put a slight delay on cursor change to make it happen later
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending); const delay = setTimeout(() => {
setCurrentReference(openReferenceValue); clearTimeout(delay);
if (inputRef.current)
// add 1 in order to prevent referenceOpen from being triggered by handleKeyUp inputRef.current.selectionEnd =
setCurrentCursorPosition(refEndAfterAppending + 1); lhsValue.length +
} else { mid.length +
// append selectedSuggestion at position after unclosed ${ closingSymbol.length +
const refEndAfterAppending = Math.min( (isEnclosed && selectedSuggestion.type === ReferenceType.SECRET ? 1 : 0); // if secret is selected the cursor should move after the closing bracket -> }
selectedSuggestion.name.length + }, 10);
leftIndexIter + setHighlightedIndex(-1); // reset highlight
2 +
Number(selectedSuggestion.type !== ReferenceType.SECRET),
rightIndexLimit - 1
);
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${selectedSuggestion.name}${
selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"
}${inputValue.slice(refEndAfterAppending)}`;
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
setCurrentReference(openReferenceValue);
setCurrentCursorPosition(refEndAfterAppending);
}
onChange?.({ target: { value: newValue } } as any);
setInputValue(newValue);
setHighlightedIndex(-1);
setIsSuggestionsOpen(false);
}; };
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const mod = (n: number, m: number) => ((n % m) + m) % m; // key operation should trigger only when popup is open
if (e.key === "ArrowDown") { if (isPopupOpen) {
setHighlightedIndex((prevIndex) => mod(prevIndex + 1, listReference.length)); if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
} else if (e.key === "ArrowUp") { setHighlightedIndex((prevIndex) => {
setHighlightedIndex((prevIndex) => mod(prevIndex - 1, listReference.length)); const pos = mod(prevIndex + 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
});
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex - 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
});
} else if (e.key === "Enter" && highlightedIndex >= 0) { } else if (e.key === "Enter" && highlightedIndex >= 0) {
e.preventDefault();
handleSuggestionSelect(); handleSuggestionSelect();
} }
if (["ArrowDown", "ArrowUp", "Tab"].includes(e.key)) {
if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key) && isPopupOpen) {
e.preventDefault(); e.preventDefault();
} }
}
}; };
const setIsOpen = (isOpen: boolean) => { const handlePopUpOpen = () => {
setHighlightedIndex(-1); setHighlightedIndex(-1);
if (isSuggestionsOpen) {
setIsSuggestionsOpen(isOpen);
}
}; };
const handleSecretChange = (e: any) => { // to handle multiple ref for single component
// propagate event to react-hook-form onChange const handleRef = useCallback((el: HTMLTextAreaElement) => {
if (onChange) { // @ts-expect-error this is for multiple ref single component
onChange(e); inputRef.current = el;
if (ref) {
if (typeof ref === "function") {
ref(el);
} else {
// eslint-disable-next-line
ref.current = el;
} }
}
setCurrentCursorPosition(inputRef.current?.selectionStart || 0); }, []);
setInputValue(e.target.value);
};
return ( return (
<Popover.Root open={isPopupOpen} onOpenChange={setIsOpen}> <Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
<Popover.Trigger asChild> <Popover.Trigger asChild>
<SecretInput <SecretInput
{...props} {...props}
ref={inputRef} ref={handleRef}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp} value={value}
value={inputValue} onFocus={() => setIsFocused.on()}
onChange={handleSecretChange} onBlur={(evt) => {
// should not on blur when its mouse down selecting a item from suggestion
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
setIsFocused.off();
}}
onChange={(e) => onChange?.(e.target.value)}
containerClassName={containerClassName} containerClassName={containerClassName}
/> />
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
align="start" align="start"
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
className={twMerge( className="relative top-2 z-[100] max-h-64 overflow-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
"relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
)}
style={{ style={{
width: "var(--radix-popover-trigger-width)", width: "var(--radix-popover-trigger-width)"
maxHeight: "var(--radix-select-content-available-height)"
}} }}
> >
<div className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white"> <div
{listReference.map((item, i) => { className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white"
ref={popoverContentRef}
>
{suggestions.map((item, i) => {
let entryIcon; let entryIcon;
if (item.type === ReferenceType.SECRET) { if (item.type === ReferenceType.SECRET) {
entryIcon = faKey; entryIcon = faKey;
@ -333,18 +312,23 @@ export const InfisicalSecretInput = ({
<div <div
tabIndex={0} tabIndex={0}
role="button" role="button"
onMouseDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleSuggestionSelect(i);
}}
aria-label="suggestion-item"
onClick={(e) => {
inputRef.current?.focus();
e.preventDefault(); e.preventDefault();
setHighlightedIndex(i); e.stopPropagation();
handleSuggestionSelect(i); handleSuggestionSelect(i);
}} }}
onMouseEnter={() => setHighlightedIndex(i)}
style={{ pointerEvents: "auto" }} style={{ pointerEvents: "auto" }}
className="flex items-center justify-between border-mineshaft-600 text-left" className="flex items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`} key={`secret-reference-secret-${i + 1}`}
> >
<div <div
className={`${ className={`${highlightedIndex === i ? "bg-gray-600" : ""
highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`} } text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
> >
<div className="flex w-full gap-2"> <div className="flex w-full gap-2">
@ -354,7 +338,7 @@ export const InfisicalSecretInput = ({
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"} size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
/> />
</div> </div>
<div className="text-md w-10/12 truncate text-left">{item.name}</div> <div className="text-md w-10/12 truncate text-left">{item.label}</div>
</div> </div>
</div> </div>
</div> </div>
@ -364,6 +348,7 @@ export const InfisicalSecretInput = ({
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
); );
}; }
);
InfisicalSecretInput.displayName = "InfisicalSecretInput"; InfisicalSecretInput.displayName = "InfisicalSecretInput";

View File

@ -0,0 +1,26 @@
import { ReactNode } from "react";
import { faWarning, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
type Props = {
icon?: IconDefinition;
title: string;
children: ReactNode;
className?: string;
};
export const NoticeBanner = ({ icon = faWarning, title, children, className }: Props) => (
<div
className={twMerge(
"flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
className
)}
>
<FontAwesomeIcon icon={icon} className="pr-6 text-4xl text-white/80" />
<div className="flex w-full flex-col text-sm">
<div className="mb-2 text-lg font-semibold">{title}</div>
<div>{children}</div>
</div>
</div>
);

View File

@ -0,0 +1 @@
export { NoticeBanner } from "./NoticeBanner";

View File

@ -41,7 +41,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough // akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
// when break is added a line break works properly // when break is added a line break works properly
return formattedContent.concat(<br />); return formattedContent.concat(<br key={`secret-value-${formattedContent.length + 1}`} />);
}; };
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & { type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
@ -90,7 +90,10 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
aria-label="secret value" aria-label="secret value"
ref={ref} ref={ref}
className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`} className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`}
onFocus={() => setIsSecretFocused.on()} onFocus={(evt) => {
onFocus?.(evt);
setIsSecretFocused.on();
}}
disabled={isDisabled} disabled={isDisabled}
spellCheck={false} spellCheck={false}
onBlur={(evt) => { onBlur={(evt) => {

View File

@ -10,12 +10,14 @@ export * from "./Drawer";
export * from "./Dropdown"; export * from "./Dropdown";
export * from "./EmailServiceSetupModal"; export * from "./EmailServiceSetupModal";
export * from "./EmptyState"; export * from "./EmptyState";
export * from "./FontAwesomeSymbol";
export * from "./FormControl"; export * from "./FormControl";
export * from "./HoverCardv2"; export * from "./HoverCardv2";
export * from "./IconButton"; export * from "./IconButton";
export * from "./Input"; export * from "./Input";
export * from "./Menu"; export * from "./Menu";
export * from "./Modal"; export * from "./Modal";
export * from "./NoticeBanner";
export * from "./Pagination"; export * from "./Pagination";
export * from "./Popoverv2"; export * from "./Popoverv2";
export * from "./SecretInput"; export * from "./SecretInput";

View File

@ -5,6 +5,7 @@ export type TServerConfig = {
isMigrationModeOn?: boolean; isMigrationModeOn?: boolean;
trustSamlEmails: boolean; trustSamlEmails: boolean;
trustLdapEmails: boolean; trustLdapEmails: boolean;
isSecretScanningDisabled: boolean;
}; };
export type TCreateAdminUserDTO = { export type TCreateAdminUserDTO = {

View File

@ -2,5 +2,6 @@ import { IdentityAuthMethod } from "./enums";
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth" [IdentityAuthMethod.AWS_AUTH]: "AWS Auth"
}; };

View File

@ -1,4 +1,5 @@
export enum IdentityAuthMethod { export enum IdentityAuthMethod {
UNIVERSAL_AUTH = "universal-auth", UNIVERSAL_AUTH = "universal-auth",
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth" AWS_AUTH = "aws-auth"
} }

View File

@ -2,6 +2,7 @@ export { identityAuthToNameMap } from "./constants";
export { IdentityAuthMethod } from "./enums"; export { IdentityAuthMethod } from "./enums";
export { export {
useAddIdentityAwsAuth, useAddIdentityAwsAuth,
useAddIdentityGcpAuth,
useAddIdentityUniversalAuth, useAddIdentityUniversalAuth,
useCreateIdentity, useCreateIdentity,
useCreateIdentityUniversalAuthClientSecret, useCreateIdentityUniversalAuthClientSecret,
@ -9,10 +10,11 @@ export {
useRevokeIdentityUniversalAuthClientSecret, useRevokeIdentityUniversalAuthClientSecret,
useUpdateIdentity, useUpdateIdentity,
useUpdateIdentityAwsAuth, useUpdateIdentityAwsAuth,
useUpdateIdentityUniversalAuth useUpdateIdentityGcpAuth,
} from "./mutations"; useUpdateIdentityUniversalAuth} from "./mutations";
export { export {
useGetIdentityAwsAuth, useGetIdentityAwsAuth,
useGetIdentityGcpAuth,
useGetIdentityUniversalAuth, useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets useGetIdentityUniversalAuthClientSecrets
} from "./queries"; } from "./queries";

View File

@ -6,6 +6,7 @@ import { organizationKeys } from "../organization/queries";
import { identitiesKeys } from "./queries"; import { identitiesKeys } from "./queries";
import { import {
AddIdentityAwsAuthDTO, AddIdentityAwsAuthDTO,
AddIdentityGcpAuthDTO,
AddIdentityUniversalAuthDTO, AddIdentityUniversalAuthDTO,
ClientSecretData, ClientSecretData,
CreateIdentityDTO, CreateIdentityDTO,
@ -15,9 +16,11 @@ import {
DeleteIdentityUniversalAuthClientSecretDTO, DeleteIdentityUniversalAuthClientSecretDTO,
Identity, Identity,
IdentityAwsAuth, IdentityAwsAuth,
IdentityGcpAuth,
IdentityUniversalAuth, IdentityUniversalAuth,
UpdateIdentityAwsAuthDTO, UpdateIdentityAwsAuthDTO,
UpdateIdentityDTO, UpdateIdentityDTO,
UpdateIdentityGcpAuthDTO,
UpdateIdentityUniversalAuthDTO UpdateIdentityUniversalAuthDTO
} from "./types"; } from "./types";
@ -173,6 +176,82 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => {
}); });
}; };
export const useAddIdentityGcpAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityGcpAuth, {}, AddIdentityGcpAuthDTO>({
mutationFn: async ({
identityId,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityGcpAuth }
} = await apiRequest.post<{ identityGcpAuth: IdentityGcpAuth }>(
`/api/v1/auth/gcp-auth/identities/${identityId}`,
{
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityGcpAuth;
},
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
}
});
};
export const useUpdateIdentityGcpAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityGcpAuth, {}, UpdateIdentityGcpAuthDTO>({
mutationFn: async ({
identityId,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityGcpAuth }
} = await apiRequest.patch<{ identityGcpAuth: IdentityGcpAuth }>(
`/api/v1/auth/gcp-auth/identities/${identityId}`,
{
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityGcpAuth;
},
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
}
});
};
export const useAddIdentityAwsAuth = () => { export const useAddIdentityAwsAuth = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<IdentityAwsAuth, {}, AddIdentityAwsAuthDTO>({ return useMutation<IdentityAwsAuth, {}, AddIdentityAwsAuthDTO>({
@ -236,6 +315,7 @@ export const useUpdateIdentityAwsAuth = () => {
accessTokenTrustedIps accessTokenTrustedIps
} }
); );
return identityAwsAuth; return identityAwsAuth;
}, },
onSuccess: (_, { organizationId }) => { onSuccess: (_, { organizationId }) => {

View File

@ -2,13 +2,14 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { ClientSecretData, IdentityAwsAuth, IdentityUniversalAuth } from "./types"; import { ClientSecretData, IdentityAwsAuth, IdentityGcpAuth, IdentityUniversalAuth } from "./types";
export const identitiesKeys = { export const identitiesKeys = {
getIdentityUniversalAuth: (identityId: string) => getIdentityUniversalAuth: (identityId: string) =>
[{ identityId }, "identity-universal-auth"] as const, [{ identityId }, "identity-universal-auth"] as const,
getIdentityUniversalAuthClientSecrets: (identityId: string) => getIdentityUniversalAuthClientSecrets: (identityId: string) =>
[{ identityId }, "identity-universal-auth-client-secrets"] as const, [{ identityId }, "identity-universal-auth-client-secrets"] as const,
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const
}; };
@ -42,6 +43,21 @@ export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) =>
}); });
}; };
export const useGetIdentityGcpAuth = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityGcpAuth(identityId),
queryFn: async () => {
const {
data: { identityGcpAuth }
} = await apiRequest.get<{ identityGcpAuth: IdentityGcpAuth }>(
`/api/v1/auth/gcp-auth/identities/${identityId}`
);
return identityGcpAuth;
}
});
};
export const useGetIdentityAwsAuth = (identityId: string) => { export const useGetIdentityAwsAuth = (identityId: string) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId), enabled: Boolean(identityId),

View File

@ -113,6 +113,48 @@ export type UpdateIdentityUniversalAuthDTO = {
}[]; }[];
}; };
export type IdentityGcpAuth = {
identityId: string;
type: "iam" | "gce";
allowedServiceAccounts: string;
allowedProjects: string;
allowedZones: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
};
export type AddIdentityGcpAuthDTO = {
organizationId: string;
identityId: string;
type: "iam" | "gce";
allowedServiceAccounts: string;
allowedProjects: string;
allowedZones: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: {
ipAddress: string;
}[];
};
export type UpdateIdentityGcpAuthDTO = {
organizationId: string;
identityId: string;
type?: "iam" | "gce";
allowedServiceAccounts?: string;
allowedProjects?: string;
allowedZones?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: {
ipAddress: string;
}[];
};
export type IdentityAwsAuth = { export type IdentityAwsAuth = {
identityId: string; identityId: string;
type: "iam"; type: "iam";

View File

@ -16,6 +16,7 @@ import {
TGetFoldersByEnvDTO, TGetFoldersByEnvDTO,
TGetProjectFoldersDTO, TGetProjectFoldersDTO,
TSecretFolder, TSecretFolder,
TUpdateFolderBatchDTO,
TUpdateFolderDTO TUpdateFolderDTO
} from "./types"; } from "./types";
@ -190,3 +191,43 @@ export const useDeleteFolder = () => {
} }
}); });
}; };
export const useUpdateFolderBatch = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateFolderBatchDTO>({
mutationFn: async ({ projectSlug, folders }) => {
const { data } = await apiRequest.patch("/api/v1/folders/batch", {
projectSlug,
folders
});
return data;
},
onSuccess: (_, { projectId, folders }) => {
folders.forEach((folder) => {
queryClient.invalidateQueries(
folderQueryKeys.getSecretFolders({
projectId,
environment: folder.environment,
path: folder.path
})
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({
workspaceId: projectId,
environment: folder.environment,
directory: folder.path
})
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({
workspaceId: projectId,
environment: folder.environment,
directory: folder.path
})
);
});
}
});
};

View File

@ -36,3 +36,14 @@ export type TDeleteFolderDTO = {
folderId: string; folderId: string;
path?: string; path?: string;
}; };
export type TUpdateFolderBatchDTO = {
projectId: string;
projectSlug: string;
folders: {
name: string;
environment: string;
id: string;
path?: string;
}[];
};

View File

@ -1,4 +1,5 @@
export { export {
useBackfillSecretReference,
useCreateSecretBatch, useCreateSecretBatch,
useCreateSecretV3, useCreateSecretV3,
useDeleteSecretBatch, useDeleteSecretBatch,

View File

@ -379,3 +379,13 @@ export const createSecret = async (dto: CreateSecretDTO) => {
const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto); const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto);
return data; return data;
}; };
export const useBackfillSecretReference = () =>
useMutation<{ message: string }, {}, { projectId: string }>({
mutationFn: async ({ projectId }) => {
const { data } = await apiRequest.post("/api/v3/secrets/backfill-secret-references", {
projectId
});
return data.message;
}
});

View File

@ -21,9 +21,7 @@ import {
faNetworkWired, faNetworkWired,
faPlug, faPlug,
faPlus, faPlus,
faUserPlus, faUserPlus
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
@ -56,7 +54,6 @@ import {
fetchOrgUsers, fetchOrgUsers,
useAddUserToWsNonE2EE, useAddUserToWsNonE2EE,
useCreateWorkspace, useCreateWorkspace,
useGetUserAction,
useRegisterUserAction useRegisterUserAction
} from "@app/hooks/api"; } from "@app/hooks/api";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
@ -312,8 +309,7 @@ const LearningItem = ({
href={link} href={link}
> >
<div <div
className={`${ className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} mb-3 rounded-md`} } mb-3 rounded-md`}
> >
<div <div
@ -325,8 +321,7 @@ const LearningItem = ({
await registerUserAction.mutateAsync(userAction); await registerUserAction.mutateAsync(userAction);
} }
}} }}
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete
complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
} text-mineshaft-100 duration-200`} } text-mineshaft-100 duration-200`}
@ -407,8 +402,7 @@ const LearningItemSquare = ({
href={link} href={link}
> >
<div <div
className={`${ className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} w-full rounded-md`} } w-full rounded-md`}
> >
<div <div
@ -420,8 +414,7 @@ const LearningItemSquare = ({
await registerUserAction.mutateAsync(userAction); await registerUserAction.mutateAsync(userAction);
} }
}} }}
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${ className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete
complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
} text-mineshaft-100 duration-200`} } text-mineshaft-100 duration-200`}
@ -438,8 +431,7 @@ const LearningItemSquare = ({
</div> </div>
)} )}
<div <div
className={`text-right text-sm font-normal text-mineshaft-300 ${ className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "font-semibold text-primary" : ""
complete ? "font-semibold text-primary" : ""
}`} }`}
> >
{complete ? "Complete!" : `About ${time}`} {complete ? "Complete!" : `About ${time}`}
@ -483,12 +475,6 @@ const OrganizationPage = withPermission(
const addUsersToProject = useAddUserToWsNonE2EE(); const addUsersToProject = useAddUserToWsNonE2EE();
const { data: updateClosed } = useGetUserAction("april_13_2024_db_update_closed");
const registerUserAction = useRegisterUserAction();
const closeUpdate = async () => {
await registerUserAction.mutateAsync("april_13_2024_db_update_closed");
};
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addNewWs", "addNewWs",
"upgradePlan" "upgradePlan"
@ -594,31 +580,6 @@ const OrganizationPage = withPermission(
</div> </div>
)} )}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
{(window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080")) && (
<div
className={`${
!updateClosed ? "block" : "hidden"
} mb-4 flex w-full flex-row items-center rounded-md border border-primary-600 bg-primary/10 p-2 text-base text-white`}
>
<FontAwesomeIcon icon={faWarning} className="p-6 text-4xl text-primary" />
<div className="text-sm">
<span className="text-lg font-semibold">Scheduled maintenance on May 11th 2024 </span>{" "}
<br />
Infisical will undergo scheduled maintenance for approximately 2 hour on Saturday, May 11th, 11am EST. During these hours, read
operations to Infisical will continue to function normally but no resources will be editable.
No action is required on your end your applications will continue to fetch secrets.
<br />
</div>
<button
type="button"
onClick={() => closeUpdate()}
aria-label="close"
className="flex h-full items-start text-mineshaft-100 duration-200 hover:text-red-400"
>
<FontAwesomeIcon icon={faXmark} />
</button>
</div>)}
<p className="mr-4 font-semibold text-white">Projects</p> <p className="mr-4 font-semibold text-white">Projects</p>
<div className="mt-6 flex w-full flex-row"> <div className="mt-6 flex w-full flex-row">
<Input <Input
@ -814,8 +775,7 @@ const OrganizationPage = withPermission(
</div> </div>
</div> </div>
<div <div
className={`w-28 pr-4 text-right text-sm font-semibold ${ className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"
false && "text-green"
}`} }`}
> >
About 2 min About 2 min

View File

@ -3,8 +3,8 @@ import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button, NoticeBanner } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useServerConfig } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components"; import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
@ -17,6 +17,7 @@ const SecretScanning = withPermission(
const router = useRouter(); const router = useRouter();
const queryParams = router.query; const queryParams = router.query;
const [integrationEnabled, setIntegrationStatus] = useState(false); const [integrationEnabled, setIntegrationStatus] = useState(false);
const { config } = useServerConfig();
useEffect(() => { useEffect(() => {
const linkInstallation = async () => { const linkInstallation = async () => {
@ -69,6 +70,11 @@ const SecretScanning = withPermission(
<div className="mb-6 text-lg text-mineshaft-300"> <div className="mb-6 text-lg text-mineshaft-300">
Automatically monitor your GitHub activity and prevent secret leaks Automatically monitor your GitHub activity and prevent secret leaks
</div> </div>
{config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased usage.
</NoticeBanner>
)}
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6"> <div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<div className="mb-1 flex flex-row"> <div className="mb-1 flex flex-row">
@ -110,7 +116,7 @@ const SecretScanning = withPermission(
colorSchema="primary" colorSchema="primary"
onClick={generateNewIntegrationSession} onClick={generateNewIntegrationSession}
className="h-min py-2" className="h-min py-2"
isDisabled={!isAllowed} isDisabled={!isAllowed || config.isSecretScanningDisabled}
> >
Integrate with GitHub Integrate with GitHub
</Button> </Button>

View File

@ -45,6 +45,14 @@ html {
width: 1%; width: 1%;
white-space: nowrap; white-space: nowrap;
} }
.w-inherit {
width: inherit;
}
.h-inherit {
height: inherit;
}
} }
@layer components { @layer components {

View File

@ -15,6 +15,7 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = { type Props = {
@ -28,6 +29,7 @@ type Props = {
const identityAuthMethods = [ const identityAuthMethods = [
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH } { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }
]; ];
@ -75,6 +77,15 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
/> />
); );
} }
case IdentityAuthMethod.GCP_AUTH: {
return (
<IdentityGcpAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.UNIVERSAL_AUTH: { case IdentityAuthMethod.UNIVERSAL_AUTH: {
return ( return (
<IdentityUniversalAuthForm <IdentityUniversalAuthForm

View File

@ -0,0 +1,384 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityGcpAuth,
useGetIdentityGcpAuth,
useUpdateIdentityGcpAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
type: z.enum(["iam", "gce"]),
allowedServiceAccounts: z.string(),
allowedProjects: z.string(),
allowedZones: z.string(),
accessTokenTTL: z.string(),
accessTokenMaxTTL: z.string(),
accessTokenNumUsesLimit: z.string(),
accessTokenTrustedIps: z
.array(
z.object({
ipAddress: z.string().max(50)
})
)
.min(1)
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
};
};
export const IdentityGcpAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "");
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: "gce",
allowedServiceAccounts: "",
allowedProjects: "",
allowedZones: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
}
});
const watchedType = watch("type");
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
remove: removeAccessTokenTrustedIp
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
useEffect(() => {
if (data) {
reset({
type: data.type,
allowedServiceAccounts: data.allowedServiceAccounts,
allowedProjects: data.allowedProjects,
allowedZones: data.allowedZones,
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
({ ipAddress, prefix }: IdentityTrustedIp) => {
return {
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
};
}
)
});
} else {
reset({
type: "iam",
allowedServiceAccounts: "",
allowedProjects: "",
allowedZones: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}
}, [data]);
const onFormSubmit = async ({
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (data) {
await updateMutateAsync({
identityId: identityAuthMethodData.identityId,
organizationId: orgId,
type,
allowedServiceAccounts,
allowedProjects,
allowedZones,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
} else {
await addMutateAsync({
identityId: identityAuthMethodData.identityId,
organizationId: orgId,
type,
allowedServiceAccounts: allowedServiceAccounts || "",
allowedProjects: allowedProjects || "",
allowedZones: allowedZones || "",
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
}
handlePopUpToggle("identityAuthMethod", false);
createNotification({
text: `Successfully ${
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success"
});
reset();
} catch (err) {
createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="type"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
<SelectItem value="gce" key="gcp-type-gce">
GCP ID Token Auth (Recommended)
</SelectItem>
<SelectItem value="iam" key="gcpiam">
GCP IAM Auth
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="allowedServiceAccounts"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Emails"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
type="text"
/>
</FormControl>
)}
/>
{watchedType === "gce" && (
<Controller
control={control}
name="allowedProjects"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Projects"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="my-gcp-project, ..." />
</FormControl>
)}
/>
)}
{watchedType === "gce" && (
<Controller
control={control}
name="allowedZones"
render={({ field, fieldState: { error } }) => (
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
</Button>
</div>
</form>
);
};

View File

@ -8,6 +8,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
FontAwesomeSymbol,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@ -19,6 +20,7 @@ import {
TextArea, TextArea,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
@ -29,20 +31,6 @@ import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types"; import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types"; import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTag,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { memo, useEffect } from "react"; import { memo, useEffect } from "react";
@ -50,7 +38,12 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { CreateReminderForm } from "./CreateReminderForm"; import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; import {
FontAwesomeSpriteName,
formSchema,
SecretActionType,
TFormSchema
} from "./SecretListView.utils";
type Props = { type Props = {
secret: DecryptedSecret; secret: DecryptedSecret;
@ -206,7 +199,6 @@ export const SecretItem = memo(
} }
}} }}
/> />
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div <div
className={twMerge( className={twMerge(
@ -227,9 +219,12 @@ export const SecretItem = memo(
onCheckedChange={() => onToggleSecretSelect(secret.id)} onCheckedChange={() => onToggleSecretSelect(secret.id)}
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")} className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
/> />
<FontAwesomeIcon <FontAwesomeSymbol
icon={faKey} className={twMerge(
className={twMerge("ml-3 block group-hover:hidden", isSelected && "hidden")} "ml-3 block h-3.5 w-3.5 group-hover:hidden",
isSelected && "hidden"
)}
symbolName={FontAwesomeSpriteName.SecretKey}
/> />
</div> </div>
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2"> <div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
@ -278,10 +273,12 @@ export const SecretItem = memo(
key="secret-value" key="secret-value"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<SecretInput <InfisicalSecretInput
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
key="secret-value" key="secret-value"
isVisible={isVisible} isVisible={isVisible}
environment={environment}
secretPath={secretPath}
{...field} {...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2" containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/> />
@ -297,7 +294,14 @@ export const SecretItem = memo(
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5" className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
onClick={copyTokenToClipboard} onClick={copyTokenToClipboard}
> >
<FontAwesomeIcon icon={isSecValueCopied ? faCheck : faCopy} /> <FontAwesomeSymbol
className="h-3.5 w-3"
symbolName={
isSecValueCopied
? FontAwesomeSpriteName.Check
: FontAwesomeSpriteName.ClipboardCopy
}
/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<DropdownMenu> <DropdownMenu>
@ -318,7 +322,10 @@ export const SecretItem = memo(
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
<Tooltip content="Tags"> <Tooltip content="Tags">
<FontAwesomeIcon icon={faTags} /> <FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Tags}
/>
</Tooltip> </Tooltip>
</IconButton> </IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -334,7 +341,14 @@ export const SecretItem = memo(
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleTagSelect(tag)} onClick={() => handleTagSelect(tag)}
key={`${secret.id}-${tagId}`} key={`${secret.id}-${tagId}`}
icon={isTagSelected && <FontAwesomeIcon icon={faCheckCircle} />} icon={
isTagSelected && (
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.CheckedCircle}
className="h-3 w-3"
/>
)
}
iconPos="right" iconPos="right"
> >
<div className="flex items-center"> <div className="flex items-center">
@ -353,7 +367,12 @@ export const SecretItem = memo(
className="w-full" className="w-full"
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />} leftIcon={
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Tags}
className="h-3 w-3"
/>
}
onClick={onCreateTag} onClick={onCreateTag}
> >
Create a tag Create a tag
@ -379,7 +398,10 @@ export const SecretItem = memo(
isOverriden && "w-5 text-primary" isOverriden && "w-5 text-primary"
)} )}
> >
<FontAwesomeIcon icon={faCodeBranch} /> <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Override}
className="h-3.5 w-3.5"
/>
</IconButton> </IconButton>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@ -393,6 +415,7 @@ export const SecretItem = memo(
variant="plain" variant="plain"
size="md" size="md"
ariaLabel="add-reminder" ariaLabel="add-reminder"
onClick={() => setCreateReminderFormOpen.on()}
> >
<Tooltip <Tooltip
content={ content={
@ -404,9 +427,9 @@ export const SecretItem = memo(
: "Reminder" : "Reminder"
} }
> >
<FontAwesomeIcon <FontAwesomeSymbol
onClick={() => setCreateReminderFormOpen.on()} className="h-3.5 w-3.5"
icon={faClock} symbolName={FontAwesomeSpriteName.Clock}
/> />
</Tooltip> </Tooltip>
</IconButton> </IconButton>
@ -430,7 +453,10 @@ export const SecretItem = memo(
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
<Tooltip content="Comment"> <Tooltip content="Comment">
<FontAwesomeIcon icon={faComment} /> <FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Comment}
/>
</Tooltip> </Tooltip>
</IconButton> </IconButton>
</PopoverTrigger> </PopoverTrigger>
@ -466,10 +492,13 @@ export const SecretItem = memo(
ariaLabel="more" ariaLabel="more"
variant="plain" variant="plain"
size="md" size="md"
className="p-0 opacity-0 group-hover:opacity-100" className="p-0 opacity-0 group-hover:opacity-100 h-5 w-4"
onClick={() => onDetailViewSecret(secret)} onClick={() => onDetailViewSecret(secret)}
> >
<FontAwesomeIcon icon={faEllipsis} size="lg" /> <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.More}
className="h-5 w-4"
/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<ProjectPermissionCan <ProjectPermissionCan
@ -488,7 +517,10 @@ export const SecretItem = memo(
onClick={() => onDeleteSecret(secret)} onClick={() => onDeleteSecret(secret)}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
<FontAwesomeIcon icon={faClose} size="lg" /> <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close}
className="h-5 w-4"
/>
</IconButton> </IconButton>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@ -516,10 +548,12 @@ export const SecretItem = memo(
{isSubmitting ? ( {isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" /> <Spinner className="m-0 h-4 w-4 p-0" />
) : ( ) : (
<FontAwesomeIcon <FontAwesomeSymbol
icon={faCheck} symbolName={FontAwesomeSpriteName.Check}
size="lg" className={twMerge(
className={twMerge("text-primary", errors.key && "text-mineshaft-300")} "h-4 w-4 text-primary",
errors.key && "text-mineshaft-300"
)}
/> />
)} )}
</IconButton> </IconButton>
@ -536,7 +570,10 @@ export const SecretItem = memo(
onClick={() => reset()} onClick={() => reset()}
isDisabled={isSubmitting} isDisabled={isSubmitting}
> >
<FontAwesomeIcon icon={faClose} size="lg" /> <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close}
className="h-4 w-4 text-primary"
/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</motion.div> </motion.div>

View File

@ -1,4 +1,5 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@ -17,6 +18,7 @@ import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPa
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types"; import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
import { SecretDetailSidebar } from "./SecretDetaiSidebar"; import { SecretDetailSidebar } from "./SecretDetaiSidebar";
import { SecretItem } from "./SecretItem"; import { SecretItem } from "./SecretItem";
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
type Props = { type Props = {
secrets?: DecryptedSecret[]; secrets?: DecryptedSecret[];
@ -89,7 +91,6 @@ export const SecretListView = ({
isVisible, isVisible,
isProtectedBranch = false isProtectedBranch = false
}: Props) => { }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteSecret", "deleteSecret",
@ -341,6 +342,13 @@ export const SecretListView = ({
> >
{namespace} {namespace}
</div> </div>
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
<FontAwesomeIcon
icon={icon}
symbol={symbol}
key={`font-awesome-svg-spritie-${symbol}`}
/>
))}
{filteredSecrets.map((secret) => ( {filteredSecrets.map((secret) => (
<SecretItem <SecretItem
environment={environment} environment={environment}

View File

@ -1,4 +1,16 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { z } from "zod"; import { z } from "zod";
export enum SecretActionType { export enum SecretActionType {
@ -41,3 +53,31 @@ export const formSchema = z.object({
}); });
export type TFormSchema = z.infer<typeof formSchema>; export type TFormSchema = z.infer<typeof formSchema>;
export enum FontAwesomeSpriteName {
SecretKey = "secret-key",
Check = "check",
ClipboardCopy = "clipboard-copy",
Tags = "secret-tags",
Clock = "reminder-clock",
Comment = "comment",
More = "more",
Override = "secret-override",
Close = "close",
CheckedCircle = "check-circle"
}
// this is an optimization technique
// https://docs.fontawesome.com/web/add-icons/svg-symbols
export const FontAwesomeSpriteSymbols = [
{ icon: faKey, symbol: FontAwesomeSpriteName.SecretKey },
{ icon: faCheck, symbol: FontAwesomeSpriteName.Check },
{ icon: faCopy, symbol: FontAwesomeSpriteName.ClipboardCopy },
{ icon: faTags, symbol: FontAwesomeSpriteName.Tags },
{ icon: faClock, symbol: FontAwesomeSpriteName.Clock },
{ icon: faComment, symbol: FontAwesomeSpriteName.Comment },
{ icon: faEllipsis, symbol: FontAwesomeSpriteName.More },
{ icon: faCodeBranch, symbol: FontAwesomeSpriteName.Override },
{ icon: faClose, symbol: FontAwesomeSpriteName.Close },
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle }
];

View File

@ -47,6 +47,7 @@ import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useOrganization, useOrganization,
useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
@ -61,6 +62,9 @@ import {
useGetUserWsKey, useGetUserWsKey,
useUpdateSecretV3 useUpdateSecretV3
} from "@app/hooks/api"; } from "@app/hooks/api";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
import { TSecretFolder } from "@app/hooks/api/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm"; import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
@ -87,6 +91,7 @@ export const SecretOverviewPage = () => {
const parentTableRef = useRef<HTMLTableElement>(null); const parentTableRef = useRef<HTMLTableElement>(null);
const [expandableTableWidth, setExpandableTableWidth] = useState(0); const [expandableTableWidth, setExpandableTableWidth] = useState(0);
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
const { permission } = useProjectPermission();
useEffect(() => { useEffect(() => {
if (parentTableRef.current) { if (parentTableRef.current) {
@ -201,11 +206,13 @@ export const SecretOverviewPage = () => {
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3(); const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: updateFolderBatch } = useUpdateFolderBatch();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addSecretsInAllEnvs", "addSecretsInAllEnvs",
"addFolder", "addFolder",
"misc" "misc",
"updateFolder"
] as const); ] as const);
const handleFolderCreate = async (folderName: string) => { const handleFolderCreate = async (folderName: string) => {
@ -236,6 +243,59 @@ export const SecretOverviewPage = () => {
} }
}; };
const handleFolderUpdate = async (newFolderName: string) => {
const { name: oldFolderName } = popUp.updateFolder.data as TSecretFolder;
const updatedFolders: TUpdateFolderBatchDTO["folders"] = [];
userAvailableEnvs.forEach((env) => {
if (
permission.can(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath })
)
) {
const folder = getFolderByNameAndEnv(oldFolderName, env.slug);
if (folder) {
updatedFolders.push({
environment: env.slug,
name: newFolderName,
id: folder.id,
path: secretPath
});
}
}
});
if (updatedFolders.length === 0) {
createNotification({
type: "info",
text: "You don't have access to rename selected folder"
});
handlePopUpClose("updateFolder");
return;
}
try {
await updateFolderBatch({
projectSlug,
folders: updatedFolders,
projectId: workspaceId
});
createNotification({
type: "success",
text: "Successfully renamed folder across environments"
});
} catch (err) {
createNotification({
type: "error",
text: "Failed to rename folder across environments"
});
} finally {
handlePopUpClose("updateFolder");
}
};
const handleSecretCreate = async (env: string, key: string, value: string) => { const handleSecretCreate = async (env: string, key: string, value: string) => {
try { try {
// create folder if not existing // create folder if not existing
@ -726,6 +786,9 @@ export const SecretOverviewPage = () => {
environments={visibleEnvs} environments={visibleEnvs}
key={`overview-${folderName}-${index + 1}`} key={`overview-${folderName}-${index + 1}`}
onClick={handleFolderClick} onClick={handleFolderClick}
onToggleFolderEdit={(name: string) =>
handlePopUpOpen("updateFolder", { name })
}
/> />
))} ))}
{!isTableLoading && {!isTableLoading &&
@ -800,6 +863,18 @@ export const SecretOverviewPage = () => {
<FolderForm onCreateFolder={handleFolderCreate} /> <FolderForm onCreateFolder={handleFolderCreate} />
</ModalContent> </ModalContent>
</Modal> </Modal>
<Modal
isOpen={popUp.updateFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("updateFolder", isOpen)}
>
<ModalContent title="Edit Folder Name">
<FolderForm
isEdit
defaultFolderName={(popUp.updateFolder?.data as Pick<TSecretFolder, "name">)?.name}
onUpdateFolder={handleFolderUpdate}
/>
</ModalContent>
</Modal>
</> </>
); );
}; };

View File

@ -1,8 +1,8 @@
import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faFolder, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Checkbox, Td, Tr } from "@app/components/v2"; import { Checkbox, IconButton, Td, Tr } from "@app/components/v2";
type Props = { type Props = {
folderName: string; folderName: string;
@ -11,6 +11,7 @@ type Props = {
onClick: (path: string) => void; onClick: (path: string) => void;
isSelected: boolean; isSelected: boolean;
onToggleFolderSelect: (folderName: string) => void; onToggleFolderSelect: (folderName: string) => void;
onToggleFolderEdit: (name: string) => void;
}; };
export const SecretOverviewFolderRow = ({ export const SecretOverviewFolderRow = ({
@ -19,6 +20,7 @@ export const SecretOverviewFolderRow = ({
isFolderPresentInEnv, isFolderPresentInEnv,
isSelected, isSelected,
onToggleFolderSelect, onToggleFolderSelect,
onToggleFolderEdit,
onClick onClick
}: Props) => { }: Props) => {
return ( return (
@ -43,6 +45,18 @@ export const SecretOverviewFolderRow = ({
/> />
</div> </div>
<div>{folderName}</div> <div>{folderName}</div>
<IconButton
ariaLabel="edit-folder"
variant="plain"
size="sm"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={(e) => {
onToggleFolderEdit(folderName);
e.stopPropagation();
}}
>
<FontAwesomeIcon icon={faPencil} size="sm" />
</IconButton>
</div> </div>
</Td> </Td>
{environments.map(({ slug }, i) => { {environments.map(({ slug }, i) => {

View File

@ -0,0 +1,43 @@
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { useBackfillSecretReference } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
export const BackfillSecretReferenceSecretion = () => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const backfillSecretReferences = useBackfillSecretReference();
if (!currentWorkspace) return null;
const handleBackfill = async () => {
if (backfillSecretReferences.isLoading) return;
try {
await backfillSecretReferences.mutateAsync({ projectId: currentWorkspace.id || "" });
createNotification({ text: "Successfully re-indexed secret references", type: "success" });
} catch {
createNotification({ text: "Failed to re-index secret references", type: "error" });
}
};
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Index Secret References</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
This will index all secret references, enabling integrations to be triggered when their values change going forward.
</p>
<Button
variant="outline_bg"
isLoading={backfillSecretReferences.isLoading}
onClick={handleBackfill}
isDisabled={!isAdmin}
>
Index Secret References
</Button>
</div>
);
};

View File

@ -0,0 +1 @@
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";

View File

@ -1,4 +1,5 @@
import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { DeleteProjectSection } from "../DeleteProjectSection"; import { DeleteProjectSection } from "../DeleteProjectSection";
import { E2EESection } from "../E2EESection"; import { E2EESection } from "../E2EESection";
import { EnvironmentSection } from "../EnvironmentSection"; import { EnvironmentSection } from "../EnvironmentSection";
@ -13,6 +14,7 @@ export const ProjectGeneralTab = () => {
<SecretTagsSection /> <SecretTagsSection />
<AutoCapitalizationSection /> <AutoCapitalizationSection />
<E2EESection /> <E2EESection />
<BackfillSecretReferenceSecretion />
<DeleteProjectSection /> <DeleteProjectSection />
</div> </div>
); );

View File

@ -1,4 +1,5 @@
export { AutoCapitalizationSection } from "./AutoCapitalizationSection"; export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
export { DeleteProjectSection } from "./DeleteProjectSection"; export { DeleteProjectSection } from "./DeleteProjectSection";
export { E2EESection } from "./E2EESection"; export { E2EESection } from "./E2EESection";
export { EnvironmentSection } from "./EnvironmentSection"; export { EnvironmentSection } from "./EnvironmentSection";