mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
65 Commits
misc/updat
...
patch-upda
Author | SHA1 | Date | |
---|---|---|---|
4a35623956 | |||
74fe673724 | |||
399ca7a221 | |||
4afd95fe1a | |||
3cd719f6b0 | |||
c6352cc970 | |||
d4555f9698 | |||
393964c4ae | |||
c438479246 | |||
9828cbbfbe | |||
fc1dffd7e2 | |||
55f8198a2d | |||
4d166402df | |||
19edf83dbc | |||
13f6b238e7 | |||
8dee1f8fc7 | |||
3b23035dfb | |||
0c8ef13d8d | |||
389d51fa5c | |||
638208e9fa | |||
c176d1e4f7 | |||
91a23a608e | |||
c6a25271dd | |||
0f5c1340d3 | |||
ecbdae110d | |||
8ef727b4ec | |||
c6f24dbb5e | |||
18c0d2fd6f | |||
c1fb8f47bf | |||
990eddeb32 | |||
ce01f8d099 | |||
faf6708b00 | |||
a58d6ebdac | |||
818b136836 | |||
0cdade6a2d | |||
bcf9b68e2b | |||
6aa9fb6ecd | |||
38e7382d85 | |||
95e12287c2 | |||
c6d14a4bea | |||
0a91586904 | |||
6561a9c7be | |||
86aaa486b4 | |||
9880977098 | |||
b93aaffe77 | |||
1ea0d55dd1 | |||
3fff272cb3 | |||
2559809eac | |||
f32abbdc25 | |||
a6f750fafb | |||
610f474ecc | |||
03f4a699e6 | |||
533d49304a | |||
184b59ad1d | |||
3113e40d0b | |||
2406d3d904 | |||
e99182c141 | |||
522dd0836e | |||
e461787c78 | |||
ac469dbe4f | |||
d98430fe07 | |||
82bafd02bb | |||
1d40d9e448 | |||
e96ca8d355 | |||
4d74d264dd |
@ -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
|
||||||
|
252
backend/package-lock.json
generated
252
backend/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -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;
|
||||||
|
14
backend/src/@types/knex.d.ts
vendored
14
backend/src/@types/knex.d.ts
vendored
@ -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,
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
27
backend/src/db/schemas/identity-gcp-auths.ts
Normal file
27
backend/src/db/schemas/identity-gcp-auths.ts
Normal 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>>;
|
@ -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";
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
21
backend/src/db/schemas/secret-references.ts
Normal file
21
backend/src/db/schemas/secret-references.ts
Normal 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>>;
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -90,15 +90,17 @@ export const secretScanningServiceFactory = ({
|
|||||||
const {
|
const {
|
||||||
data: { repositories }
|
data: { repositories }
|
||||||
} = await octokit.apps.listReposAccessibleToInstallation();
|
} = await octokit.apps.listReposAccessibleToInstallation();
|
||||||
await Promise.all(
|
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||||
repositories.map(({ id, full_name }) =>
|
await Promise.all(
|
||||||
secretScanningQueue.startFullRepoScan({
|
repositories.map(({ id, full_name }) =>
|
||||||
organizationId: session.orgId,
|
secretScanningQueue.startFullRepoScan({
|
||||||
installationId,
|
organizationId: session.orgId,
|
||||||
repository: { id, fullName: full_name }
|
installationId,
|
||||||
})
|
repository: { id, fullName: full_name }
|
||||||
)
|
})
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
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,13 +164,15 @@ export const secretScanningServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (!installationLink) return;
|
if (!installationLink) return;
|
||||||
|
|
||||||
await secretScanningQueue.startPushEventScan({
|
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||||
commits,
|
await secretScanningQueue.startPushEventScan({
|
||||||
pusher: { name: pusher.name, email: pusher.email },
|
commits,
|
||||||
repository: { fullName: repository.full_name, id: repository.id },
|
pusher: { name: pusher.name, email: pusher.email },
|
||||||
organizationId: installationLink.orgId,
|
repository: { fullName: repository.full_name, id: repository.id },
|
||||||
installationId: String(installation?.id)
|
organizationId: installationLink.orgId,
|
||||||
});
|
installationId: String(installation?.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRepoDeleteEvent = async (installationId: string, repositoryIds: string[]) => {
|
const handleRepoDeleteEvent = async (installationId: string, repositoryIds: string[]) => {
|
||||||
|
@ -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: {
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
throw new Error("Infisical is in maintenance mode. Please try again later.");
|
// 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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 }
|
||||||
) => {
|
) => {
|
||||||
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||||
|
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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
268
backend/src/server/routes/v1/identity-gcp-auth-router.ts
Normal file
268
backend/src/server/routes/v1/identity-gcp-auth-router.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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({
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -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 };
|
||||||
|
};
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
@ -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(", ");
|
||||||
|
});
|
@ -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, rolePermission);
|
||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
|
||||||
if (!hasRequiredPriviledges)
|
if (!hasRequiredPriviledges) {
|
||||||
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
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(
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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,50 +636,39 @@ 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) {
|
return {
|
||||||
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
|
type: secret.type,
|
||||||
const sharedExist = await secretDAL.findOne({
|
userId: secret.type === SecretType.Personal ? userId : null,
|
||||||
secretBlindIndex: keyName2BlindIndex[secret.secretName],
|
secretName: secret.secretName,
|
||||||
folderId,
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
type: SecretType.Shared
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
});
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag,
|
||||||
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
|
tags: secret.tags,
|
||||||
|
references: secretReferences
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (!sharedExist)
|
// get all tags
|
||||||
throw new BadRequestError({
|
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
|
||||||
message: "Failed to create personal secret override for no corresponding shared secret"
|
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
|
||||||
});
|
if (tags.length !== tagIds.length) throw new BadRequestError({ message: "Tag not found" });
|
||||||
}
|
|
||||||
|
|
||||||
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
|
|
||||||
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: secret.type,
|
|
||||||
userId: secret.type === SecretType.Personal ? userId : null,
|
|
||||||
secretName: secret.secretName,
|
|
||||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
|
||||||
secretKeyIV: secretKeyEncrypted.iv,
|
|
||||||
secretKeyTag: secretKeyEncrypted.tag,
|
|
||||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
|
||||||
secretValueIV: secretValueEncrypted.iv,
|
|
||||||
secretValueTag: secretValueEncrypted.tag,
|
|
||||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
|
||||||
secretCommentIV: secretCommentEncrypted.iv,
|
|
||||||
secretCommentTag: secretCommentEncrypted.tag,
|
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
|
||||||
tags: secret.tags
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSecrets = await secretDAL.transaction(async (tx) =>
|
const newSecrets = await secretDAL.transaction(async (tx) =>
|
||||||
fnSecretBulkInsert({
|
fnSecretBulkInsert({
|
||||||
@ -703,56 +737,35 @@ 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 secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
|
const secretReferences = getAllNestedSecretReferences(secret.secretValue || "");
|
||||||
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
|
||||||
|
|
||||||
if (secret.type === SecretType.Personal) {
|
return {
|
||||||
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
|
type: secret.type,
|
||||||
|
userId: secret.type === SecretType.Personal ? userId : null,
|
||||||
const sharedExist = await secretDAL.findOne({
|
secretName: secret.secretName,
|
||||||
secretBlindIndex: keyName2BlindIndex[secret.secretName],
|
newSecretName: secret.newSecretName,
|
||||||
folderId,
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
type: SecretType.Shared
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
});
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
if (!sharedExist)
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
throw new BadRequestError({
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
message: "Failed to update personal secret override for no corresponding shared secret"
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
});
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag,
|
||||||
if (secret.newSecretName)
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
throw new BadRequestError({ message: "Personal secret cannot change the key name" });
|
tags: secret.tags,
|
||||||
}
|
references: secretReferences
|
||||||
|
};
|
||||||
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
|
});
|
||||||
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: secret.type,
|
|
||||||
userId: secret.type === SecretType.Personal ? userId : null,
|
|
||||||
secretName: secret.secretName,
|
|
||||||
newSecretName: secret.newSecretName,
|
|
||||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
|
||||||
secretKeyIV: secretKeyEncrypted.iv,
|
|
||||||
secretKeyTag: secretKeyEncrypted.tag,
|
|
||||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
|
||||||
secretValueIV: secretValueEncrypted.iv,
|
|
||||||
secretValueTag: secretValueEncrypted.tag,
|
|
||||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
|
||||||
secretCommentIV: secretCommentEncrypted.iv,
|
|
||||||
secretCommentTag: secretCommentEncrypted.tag,
|
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
|
||||||
tags: secret.tags
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
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) : [];
|
||||||
|
@ -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);
|
||||||
|
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}]`
|
||||||
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
imports
|
imports
|
||||||
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
|
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
|
||||||
.map(({ folderId }) => {
|
// filter out already synced ones
|
||||||
const syncDto = {
|
.filter(
|
||||||
|
({ folderId }) =>
|
||||||
|
!deDupeQueue[
|
||||||
|
uniqueIntegrationKey(
|
||||||
|
foldersGroupedById[folderId][0].environmentSlug,
|
||||||
|
foldersGroupedById[folderId][0].path
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.map(({ folderId }) =>
|
||||||
|
syncSecrets({
|
||||||
depth: depth + 1,
|
depth: depth + 1,
|
||||||
projectId,
|
projectId,
|
||||||
secretPath: foldersGroupedById[folderId][0].path,
|
secretPath: foldersGroupedById[folderId][0].path,
|
||||||
environment: foldersGroupedById[folderId][0].environmentSlug
|
environment: foldersGroupedById[folderId][0].environmentSlug,
|
||||||
};
|
deDupeQueue
|
||||||
logger.info(
|
})
|
||||||
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
)
|
||||||
);
|
);
|
||||||
return syncSecrets(syncDto);
|
}
|
||||||
})
|
|
||||||
|
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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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 = {
|
||||||
|
@ -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.
|
||||||
|
351
docs/documentation/platform/identities/gcp-auth.mdx
Normal file
351
docs/documentation/platform/identities/gcp-auth.mdx
Normal 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**.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
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**.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|

|
||||||
|
</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**.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
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**.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|

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

|

|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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 |
@ -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
|
||||||
|
@ -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",
|
||||||
{
|
{
|
||||||
|
@ -176,7 +176,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
|
|||||||

|

|
||||||
</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
|
||||||
|
@ -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 }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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";
|
1
frontend/src/components/v2/FontAwesomeSymbol/index.tsx
Normal file
1
frontend/src/components/v2/FontAwesomeSymbol/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { FontAwesomeSymbol } from "./FontAwesomeSymbol";
|
@ -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,339 +57,298 @@ 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,
|
(
|
||||||
containerClassName,
|
{
|
||||||
secretPath: propSecretPath,
|
value = "",
|
||||||
environment: propEnvironment,
|
onChange,
|
||||||
onChange,
|
containerClassName,
|
||||||
...props
|
secretPath: propSecretPath,
|
||||||
}: Props) => {
|
environment: propEnvironment,
|
||||||
const [inputValue, setInputValue] = useState(propValue ?? "");
|
...props
|
||||||
const [isSuggestionsOpen, setIsSuggestionsOpen] = useState(false);
|
},
|
||||||
const [currentCursorPosition, setCurrentCursorPosition] = useState(0);
|
ref
|
||||||
const [currentReference, setCurrentReference] = useState<string>("");
|
) => {
|
||||||
const [secretPath, setSecretPath] = useState<string>(propSecretPath || "/");
|
const { currentWorkspace } = useWorkspace();
|
||||||
const [environment, setEnvironment] = useState<string | undefined>(propEnvironment);
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
|
||||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
|
||||||
const { data: secrets } = useGetProjectSecrets({
|
|
||||||
decryptFileKey: decryptFileKey!,
|
|
||||||
environment: environment || currentWorkspace?.environments?.[0].slug!,
|
|
||||||
secretPath,
|
|
||||||
workspaceId
|
|
||||||
});
|
|
||||||
const { folderNames: folders } = useGetFoldersByEnv({
|
|
||||||
path: secretPath,
|
|
||||||
environments: [environment || currentWorkspace?.environments?.[0].slug!],
|
|
||||||
projectId: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
const debouncedCurrentReference = useDebounce(currentReference, 100);
|
const debouncedValue = useDebounce(value, 500);
|
||||||
|
|
||||||
const [listReference, setListReference] = useState<ReferenceItem[]>([]);
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const isPopupOpen = isSuggestionsOpen && listReference.length > 0 && currentReference.length > 0;
|
|
||||||
|
|
||||||
useEffect(() => {
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
setInputValue(propValue ?? "");
|
const popoverContentRef = useRef<HTMLDivElement>(null);
|
||||||
}, [propValue]);
|
const [isFocused, setIsFocused] = useToggle(false);
|
||||||
|
const currentCursorPosition = inputRef.current?.selectionStart || 0;
|
||||||
|
|
||||||
useEffect(() => {
|
const suggestionSource = useMemo(() => {
|
||||||
let currentEnvironment = propEnvironment;
|
const left = getIndexOfUnclosedRefToTheLeft(debouncedValue, currentCursorPosition - 1);
|
||||||
let currentSecretPath = propSecretPath || "/";
|
if (left === -1) return { left, value: "", predicate: "", isDeep: false };
|
||||||
|
|
||||||
if (!currentReference) {
|
const suggestionSourceValue = debouncedValue.slice(left + 1, currentCursorPosition);
|
||||||
setSecretPath(currentSecretPath);
|
let suggestionSourceEnv: string | undefined = propEnvironment;
|
||||||
setEnvironment(currentEnvironment);
|
let suggestionSourceSecretPath: string | undefined = propSecretPath || "/";
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNested = currentReference.includes(".");
|
// 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];
|
||||||
|
}
|
||||||
|
|
||||||
if (isNested) {
|
return {
|
||||||
const [envSlug, ...folderPaths] = currentReference.split(".");
|
left: left + 1,
|
||||||
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
|
// the full value inside a ${<value>}
|
||||||
currentEnvironment = isValidEnvSlug ? envSlug : undefined;
|
value: suggestionSourceValue,
|
||||||
|
// the final part after staging.dev.<folder1>.<predicate>
|
||||||
|
predicate,
|
||||||
|
isOpen: left !== -1,
|
||||||
|
isDeep,
|
||||||
|
environment: suggestionSourceEnv,
|
||||||
|
secretPath: suggestionSourceSecretPath
|
||||||
|
};
|
||||||
|
}, [debouncedValue]);
|
||||||
|
|
||||||
// should be based on the last valid section (with .)
|
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
|
||||||
folderPaths.pop();
|
const { data: secrets } = useGetProjectSecrets({
|
||||||
currentSecretPath = `/${folderPaths?.join("/")}`;
|
decryptFileKey: decryptFileKey!,
|
||||||
}
|
environment: suggestionSource.environment || "",
|
||||||
|
secretPath: suggestionSource.secretPath || "",
|
||||||
setSecretPath(currentSecretPath);
|
workspaceId,
|
||||||
setEnvironment(currentEnvironment);
|
options: {
|
||||||
}, [debouncedCurrentReference]);
|
enabled: isPopupOpen
|
||||||
|
}
|
||||||
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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (isNested) {
|
|
||||||
folders?.forEach((folder) => {
|
|
||||||
currentListReference.unshift({ name: folder, type: ReferenceType.FOLDER });
|
|
||||||
});
|
|
||||||
} else if (environment) {
|
|
||||||
currentWorkspace?.environments.forEach((env) => {
|
|
||||||
currentListReference.unshift({
|
|
||||||
name: env.slug,
|
|
||||||
type: ReferenceType.ENVIRONMENT
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
secrets?.forEach((secret) => {
|
|
||||||
currentListReference.unshift({ name: secret.key, type: ReferenceType.SECRET });
|
|
||||||
});
|
});
|
||||||
|
const { data: folders } = useGetProjectFolders({
|
||||||
// Get fragment inside currentReference
|
environment: suggestionSource.environment || "",
|
||||||
const searchFragment = isNested ? currentReference.split(".").pop() || "" : currentReference;
|
path: suggestionSource.secretPath || "",
|
||||||
const filteredListRef = currentListReference
|
projectId: workspaceId,
|
||||||
.filter((suggestionEntry) =>
|
options: {
|
||||||
suggestionEntry.name.toUpperCase().startsWith(searchFragment.toUpperCase())
|
enabled: isPopupOpen
|
||||||
)
|
|
||||||
.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 indexIter;
|
const suggestions = useMemo(() => {
|
||||||
};
|
if (!isPopupOpen) return [];
|
||||||
|
// reset highlight whenever recomputation happens
|
||||||
|
setHighlightedIndex(-1);
|
||||||
|
const suggestionsArr: ReferenceItem[] = [];
|
||||||
|
const predicate = suggestionSource.predicate.toLowerCase();
|
||||||
|
|
||||||
const getIndexOfUnclosedRefToTheRight = (pos: number) => {
|
if (!suggestionSource.isDeep) {
|
||||||
const unclosedReferenceIndexMatches = [...inputValue.matchAll(REGEX_OPEN_SECRET_REFERENCE)].map(
|
// At first level only environments and secrets
|
||||||
(match) => match.index
|
(currentWorkspace?.environments || []).forEach(({ name, slug }) => {
|
||||||
);
|
if (name.toLowerCase().startsWith(predicate))
|
||||||
|
suggestionsArr.push({
|
||||||
// find the next unclosed reference index to the right of the current cursor position
|
label: name,
|
||||||
// this is so that we know the limitation for slicing references
|
slug,
|
||||||
let indexIter = Infinity;
|
type: ReferenceType.ENVIRONMENT
|
||||||
unclosedReferenceIndexMatches.forEach((index) => {
|
});
|
||||||
if (index !== undefined && index > pos && index < indexIter) {
|
});
|
||||||
indexIter = index;
|
} else {
|
||||||
|
// one deeper levels its based on an environment folders and secrets
|
||||||
|
(folders || []).forEach(({ name }) => {
|
||||||
|
if (name.toLowerCase().startsWith(predicate))
|
||||||
|
suggestionsArr.push({
|
||||||
|
label: name,
|
||||||
|
slug: name,
|
||||||
|
type: ReferenceType.FOLDER
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
(secrets || []).forEach(({ key }) => {
|
||||||
|
if (key.toLowerCase().startsWith(predicate))
|
||||||
|
suggestionsArr.push({
|
||||||
|
label: key,
|
||||||
|
slug: key,
|
||||||
|
type: ReferenceType.SECRET
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return suggestionsArr;
|
||||||
|
}, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
|
||||||
|
|
||||||
return indexIter;
|
const handleSuggestionSelect = (selectIndex?: number) => {
|
||||||
};
|
const selectedSuggestion =
|
||||||
|
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
|
||||||
|
if (!selectedSuggestion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const rightBracketIndex = getIndexOfUnclosedRefToTheRight(value, suggestionSource.left);
|
||||||
// open suggestions if current position is to the right of an unclosed secret reference
|
const isEnclosed = rightBracketIndex !== -1;
|
||||||
const indexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
|
// <lhsValue>${}<rhsvalue>
|
||||||
if (indexIter === -1) {
|
const lhsValue = value.slice(0, suggestionSource.left);
|
||||||
return;
|
const rhsValue = value.slice(
|
||||||
}
|
rightBracketIndex !== -1 ? rightBracketIndex + 1 : currentCursorPosition
|
||||||
|
);
|
||||||
setIsSuggestionsOpen(true);
|
// mid will be computed value inside the interpolation
|
||||||
|
const mid = suggestionSource.isDeep
|
||||||
if (e.key !== "Enter") {
|
? `${suggestionSource.value.slice(0, -suggestionSource.predicate.length || undefined)}${selectedSuggestion.slug
|
||||||
// current reference is then going to be based on the text from the closest ${ to the right
|
}`
|
||||||
// until the current cursor position
|
: selectedSuggestion.slug;
|
||||||
const openReferenceValue = inputValue.slice(indexIter + 2, currentCursorPosition);
|
// whether we should append . or closing bracket on selecting suggestion
|
||||||
setCurrentReference(openReferenceValue);
|
const closingSymbol = getClosingSymbol(
|
||||||
}
|
selectedSuggestion.type === ReferenceType.SECRET,
|
||||||
};
|
isEnclosed
|
||||||
|
|
||||||
const handleSuggestionSelect = (selectedIndex?: number) => {
|
|
||||||
const selectedSuggestion = listReference[selectedIndex ?? highlightedIndex];
|
|
||||||
|
|
||||||
if (!selectedSuggestion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const leftIndexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
|
|
||||||
const rightIndexLimit = getIndexOfUnclosedRefToTheRight(currentCursorPosition);
|
|
||||||
|
|
||||||
if (leftIndexIter === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newValue = "";
|
|
||||||
const currentOpenRef = inputValue.slice(leftIndexIter + 2, currentCursorPosition);
|
|
||||||
if (currentOpenRef.includes(".")) {
|
|
||||||
// append suggestion after last DOT (.)
|
|
||||||
const lastDotIndex = currentReference.lastIndexOf(".");
|
|
||||||
const existingPath = currentReference.slice(0, lastDotIndex);
|
|
||||||
const refEndAfterAppending = Math.min(
|
|
||||||
leftIndexIter +
|
|
||||||
3 +
|
|
||||||
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)
|
||||||
|
inputRef.current.selectionEnd =
|
||||||
|
lhsValue.length +
|
||||||
|
mid.length +
|
||||||
|
closingSymbol.length +
|
||||||
|
(isEnclosed && selectedSuggestion.type === ReferenceType.SECRET ? 1 : 0); // if secret is selected the cursor should move after the closing bracket -> }
|
||||||
|
}, 10);
|
||||||
|
setHighlightedIndex(-1); // reset highlight
|
||||||
|
};
|
||||||
|
|
||||||
// add 1 in order to prevent referenceOpen from being triggered by handleKeyUp
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
setCurrentCursorPosition(refEndAfterAppending + 1);
|
// key operation should trigger only when popup is open
|
||||||
} else {
|
if (isPopupOpen) {
|
||||||
// append selectedSuggestion at position after unclosed ${
|
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
|
||||||
const refEndAfterAppending = Math.min(
|
setHighlightedIndex((prevIndex) => {
|
||||||
selectedSuggestion.name.length +
|
const pos = mod(prevIndex + 1, suggestions.length);
|
||||||
leftIndexIter +
|
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
|
||||||
2 +
|
block: "nearest",
|
||||||
Number(selectedSuggestion.type !== ReferenceType.SECRET),
|
behavior: "smooth"
|
||||||
rightIndexLimit - 1
|
});
|
||||||
);
|
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) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSuggestionSelect();
|
||||||
|
}
|
||||||
|
if (["ArrowDown", "ArrowUp", "Tab"].includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${selectedSuggestion.name}${
|
const handlePopUpOpen = () => {
|
||||||
selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"
|
setHighlightedIndex(-1);
|
||||||
}${inputValue.slice(refEndAfterAppending)}`;
|
};
|
||||||
|
|
||||||
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
|
// to handle multiple ref for single component
|
||||||
setCurrentReference(openReferenceValue);
|
const handleRef = useCallback((el: HTMLTextAreaElement) => {
|
||||||
setCurrentCursorPosition(refEndAfterAppending);
|
// @ts-expect-error this is for multiple ref single component
|
||||||
}
|
inputRef.current = el;
|
||||||
|
if (ref) {
|
||||||
|
if (typeof ref === "function") {
|
||||||
|
ref(el);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
ref.current = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
onChange?.({ target: { value: newValue } } as any);
|
return (
|
||||||
setInputValue(newValue);
|
<Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
|
||||||
setHighlightedIndex(-1);
|
<Popover.Trigger asChild>
|
||||||
setIsSuggestionsOpen(false);
|
<SecretInput
|
||||||
};
|
{...props}
|
||||||
|
ref={handleRef}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
value={value}
|
||||||
|
onFocus={() => setIsFocused.on()}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content
|
||||||
|
align="start"
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
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"
|
||||||
|
style={{
|
||||||
|
width: "var(--radix-popover-trigger-width)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
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;
|
||||||
|
if (item.type === ReferenceType.SECRET) {
|
||||||
|
entryIcon = faKey;
|
||||||
|
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
||||||
|
entryIcon = faCircle;
|
||||||
|
} else {
|
||||||
|
entryIcon = faFolder;
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
return (
|
||||||
const mod = (n: number, m: number) => ((n % m) + m) % m;
|
|
||||||
if (e.key === "ArrowDown") {
|
|
||||||
setHighlightedIndex((prevIndex) => mod(prevIndex + 1, listReference.length));
|
|
||||||
} else if (e.key === "ArrowUp") {
|
|
||||||
setHighlightedIndex((prevIndex) => mod(prevIndex - 1, listReference.length));
|
|
||||||
} else if (e.key === "Enter" && highlightedIndex >= 0) {
|
|
||||||
handleSuggestionSelect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key) && isPopupOpen) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setIsOpen = (isOpen: boolean) => {
|
|
||||||
setHighlightedIndex(-1);
|
|
||||||
|
|
||||||
if (isSuggestionsOpen) {
|
|
||||||
setIsSuggestionsOpen(isOpen);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSecretChange = (e: any) => {
|
|
||||||
// propagate event to react-hook-form onChange
|
|
||||||
if (onChange) {
|
|
||||||
onChange(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentCursorPosition(inputRef.current?.selectionStart || 0);
|
|
||||||
setInputValue(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover.Root open={isPopupOpen} onOpenChange={setIsOpen}>
|
|
||||||
<Popover.Trigger asChild>
|
|
||||||
<SecretInput
|
|
||||||
{...props}
|
|
||||||
ref={inputRef}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
value={inputValue}
|
|
||||||
onChange={handleSecretChange}
|
|
||||||
containerClassName={containerClassName}
|
|
||||||
/>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Content
|
|
||||||
align="start"
|
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
||||||
className={twMerge(
|
|
||||||
"relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
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">
|
|
||||||
{listReference.map((item, i) => {
|
|
||||||
let entryIcon;
|
|
||||||
if (item.type === ReferenceType.SECRET) {
|
|
||||||
entryIcon = faKey;
|
|
||||||
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
|
||||||
entryIcon = faCircle;
|
|
||||||
} else {
|
|
||||||
entryIcon = faFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setHighlightedIndex(i);
|
|
||||||
handleSuggestionSelect(i);
|
|
||||||
}}
|
|
||||||
style={{ pointerEvents: "auto" }}
|
|
||||||
className="flex items-center justify-between border-mineshaft-600 text-left"
|
|
||||||
key={`secret-reference-secret-${i + 1}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`${
|
tabIndex={0}
|
||||||
highlightedIndex === i ? "bg-gray-600" : ""
|
role="button"
|
||||||
} 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`}
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") handleSuggestionSelect(i);
|
||||||
|
}}
|
||||||
|
aria-label="suggestion-item"
|
||||||
|
onClick={(e) => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSuggestionSelect(i);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHighlightedIndex(i)}
|
||||||
|
style={{ pointerEvents: "auto" }}
|
||||||
|
className="flex items-center justify-between border-mineshaft-600 text-left"
|
||||||
|
key={`secret-reference-secret-${i + 1}`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full gap-2">
|
<div
|
||||||
<div className="flex items-center text-yellow-700">
|
className={`${highlightedIndex === i ? "bg-gray-600" : ""
|
||||||
<FontAwesomeIcon
|
} 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`}
|
||||||
icon={entryIcon}
|
>
|
||||||
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
|
<div className="flex w-full gap-2">
|
||||||
/>
|
<div className="flex items-center text-yellow-700">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={entryIcon}
|
||||||
|
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-md w-10/12 truncate text-left">{item.label}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-md w-10/12 truncate text-left">{item.name}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</Popover.Content>
|
||||||
</Popover.Content>
|
</Popover.Root>
|
||||||
</Popover.Root>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
InfisicalSecretInput.displayName = "InfisicalSecretInput";
|
InfisicalSecretInput.displayName = "InfisicalSecretInput";
|
||||||
|
26
frontend/src/components/v2/NoticeBanner/NoticeBanner.tsx
Normal file
26
frontend/src/components/v2/NoticeBanner/NoticeBanner.tsx
Normal 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>
|
||||||
|
);
|
1
frontend/src/components/v2/NoticeBanner/index.tsx
Normal file
1
frontend/src/components/v2/NoticeBanner/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { NoticeBanner } from "./NoticeBanner";
|
@ -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) => {
|
||||||
|
@ -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";
|
||||||
|
@ -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 = {
|
||||||
|
@ -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"
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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),
|
||||||
|
@ -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";
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
|
useBackfillSecretReference,
|
||||||
useCreateSecretBatch,
|
useCreateSecretBatch,
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
useDeleteSecretBatch,
|
useDeleteSecretBatch,
|
||||||
|
@ -87,11 +87,11 @@ export const useCreateSecretV3 = ({
|
|||||||
|
|
||||||
const randomBytes = latestFileKey
|
const randomBytes = latestFileKey
|
||||||
? decryptAssymmetric({
|
? decryptAssymmetric({
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
nonce: latestFileKey.nonce,
|
nonce: latestFileKey.nonce,
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY
|
privateKey: PRIVATE_KEY
|
||||||
})
|
})
|
||||||
: crypto.randomBytes(16).toString("hex");
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
const reqBody = {
|
const reqBody = {
|
||||||
@ -148,11 +148,11 @@ export const useUpdateSecretV3 = ({
|
|||||||
|
|
||||||
const randomBytes = latestFileKey
|
const randomBytes = latestFileKey
|
||||||
? decryptAssymmetric({
|
? decryptAssymmetric({
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
nonce: latestFileKey.nonce,
|
nonce: latestFileKey.nonce,
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY
|
privateKey: PRIVATE_KEY
|
||||||
})
|
})
|
||||||
: crypto.randomBytes(16).toString("hex");
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
const reqBody = {
|
const reqBody = {
|
||||||
@ -244,11 +244,11 @@ export const useCreateSecretBatch = ({
|
|||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const randomBytes = latestFileKey
|
const randomBytes = latestFileKey
|
||||||
? decryptAssymmetric({
|
? decryptAssymmetric({
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
nonce: latestFileKey.nonce,
|
nonce: latestFileKey.nonce,
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY
|
privateKey: PRIVATE_KEY
|
||||||
})
|
})
|
||||||
: crypto.randomBytes(16).toString("hex");
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
const reqBody = {
|
const reqBody = {
|
||||||
@ -297,11 +297,11 @@ export const useUpdateSecretBatch = ({
|
|||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||||
const randomBytes = latestFileKey
|
const randomBytes = latestFileKey
|
||||||
? decryptAssymmetric({
|
? decryptAssymmetric({
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
nonce: latestFileKey.nonce,
|
nonce: latestFileKey.nonce,
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY
|
privateKey: PRIVATE_KEY
|
||||||
})
|
})
|
||||||
: crypto.randomBytes(16).toString("hex");
|
: crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
const reqBody = {
|
const reqBody = {
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -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,9 +309,8 @@ 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
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
@ -325,11 +321,10 @@ 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`}
|
||||||
>
|
>
|
||||||
<div className="mr-4 flex flex-row items-center">
|
<div className="mr-4 flex flex-row items-center">
|
||||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||||
@ -407,9 +402,8 @@ 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
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
@ -420,11 +414,10 @@ 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`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full flex-col items-center px-6 py-4">
|
<div className="flex w-full flex-col items-center px-6 py-4">
|
||||||
<div className="flex w-full flex-row items-start justify-between">
|
<div className="flex w-full flex-row items-start justify-between">
|
||||||
@ -438,9 +431,8 @@ 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}`}
|
||||||
</div>
|
</div>
|
||||||
@ -480,14 +472,8 @@ const OrganizationPage = withPermission(
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const routerOrgId = String(router.query.id);
|
const routerOrgId = String(router.query.id);
|
||||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
||||||
|
|
||||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
|
||||||
|
|
||||||
const { data: updateClosed } = useGetUserAction("april_13_2024_db_update_closed");
|
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||||
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",
|
||||||
@ -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
|
||||||
@ -748,95 +709,94 @@ const OrganizationPage = withPermission(
|
|||||||
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
||||||
30 * 24 * 60 * 60 * 1000
|
30 * 24 * 60 * 60 * 1000
|
||||||
) && (
|
) && (
|
||||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
||||||
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||||
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
<LearningItemSquare
|
|
||||||
text="Watch Infisical demo"
|
|
||||||
subText="Set up Infisical in 3 min."
|
|
||||||
complete={hasUserClickedIntro}
|
|
||||||
icon={faHandPeace}
|
|
||||||
time="3 min"
|
|
||||||
userAction="intro_cta_clicked"
|
|
||||||
link="https://www.youtube.com/watch?v=PK23097-25I"
|
|
||||||
/>
|
|
||||||
{orgWorkspaces.length !== 0 && (
|
|
||||||
<>
|
|
||||||
<LearningItemSquare
|
|
||||||
text="Add your secrets"
|
|
||||||
subText="Drop a .env file or type your secrets."
|
|
||||||
complete={hasUserPushedSecrets}
|
|
||||||
icon={faPlus}
|
|
||||||
time="1 min"
|
|
||||||
userAction="first_time_secrets_pushed"
|
|
||||||
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
|
|
||||||
/>
|
|
||||||
<LearningItemSquare
|
|
||||||
text="Invite your teammates"
|
|
||||||
subText="Infisical is better used as a team."
|
|
||||||
complete={usersInOrg}
|
|
||||||
icon={faUserPlus}
|
|
||||||
time="2 min"
|
|
||||||
link={`/org/${router.query.id}/members?action=invite`}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="block xl:hidden 2xl:block">
|
|
||||||
<LearningItemSquare
|
<LearningItemSquare
|
||||||
text="Join Infisical Slack"
|
text="Watch Infisical demo"
|
||||||
subText="Have any questions? Ask us!"
|
subText="Set up Infisical in 3 min."
|
||||||
complete={hasUserClickedSlack}
|
complete={hasUserClickedIntro}
|
||||||
icon={faSlack}
|
icon={faHandPeace}
|
||||||
time="1 min"
|
time="3 min"
|
||||||
userAction="slack_cta_clicked"
|
userAction="intro_cta_clicked"
|
||||||
link="https://infisical.com/slack"
|
link="https://www.youtube.com/watch?v=PK23097-25I"
|
||||||
/>
|
/>
|
||||||
|
{orgWorkspaces.length !== 0 && (
|
||||||
|
<>
|
||||||
|
<LearningItemSquare
|
||||||
|
text="Add your secrets"
|
||||||
|
subText="Drop a .env file or type your secrets."
|
||||||
|
complete={hasUserPushedSecrets}
|
||||||
|
icon={faPlus}
|
||||||
|
time="1 min"
|
||||||
|
userAction="first_time_secrets_pushed"
|
||||||
|
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
|
||||||
|
/>
|
||||||
|
<LearningItemSquare
|
||||||
|
text="Invite your teammates"
|
||||||
|
subText="Infisical is better used as a team."
|
||||||
|
complete={usersInOrg}
|
||||||
|
icon={faUserPlus}
|
||||||
|
time="2 min"
|
||||||
|
link={`/org/${router.query.id}/members?action=invite`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="block xl:hidden 2xl:block">
|
||||||
|
<LearningItemSquare
|
||||||
|
text="Join Infisical Slack"
|
||||||
|
subText="Have any questions? Ask us!"
|
||||||
|
complete={hasUserClickedSlack}
|
||||||
|
icon={faSlack}
|
||||||
|
time="1 min"
|
||||||
|
userAction="slack_cta_clicked"
|
||||||
|
link="https://infisical.com/slack"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{orgWorkspaces.length !== 0 && (
|
||||||
{orgWorkspaces.length !== 0 && (
|
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
|
||||||
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
|
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
<div className="mr-4 flex w-full flex-row items-center">
|
||||||
<div className="mr-4 flex w-full flex-row items-center">
|
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
{false && (
|
||||||
{false && (
|
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
<FontAwesomeIcon
|
||||||
<FontAwesomeIcon
|
icon={faCheckCircle}
|
||||||
icon={faCheckCircle}
|
className="h-5 w-5 text-4xl text-green"
|
||||||
className="h-5 w-5 text-4xl text-green"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
<div className="flex flex-col items-start pl-0.5">
|
||||||
<div className="flex flex-col items-start pl-0.5">
|
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
<div className="text-sm font-normal">
|
||||||
<div className="text-sm font-normal">
|
Replace .env files with a more secure and efficient alternative.
|
||||||
Replace .env files with a more secure and efficient alternative.
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
About 2 min
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<TabsObject />
|
||||||
className={`w-28 pr-4 text-right text-sm font-semibold ${
|
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||||
false && "text-green"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
About 2 min
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<TabsObject />
|
)}
|
||||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
{orgWorkspaces.length !== 0 && (
|
||||||
</div>
|
<LearningItem
|
||||||
)}
|
text="Integrate Infisical with your infrastructure"
|
||||||
{orgWorkspaces.length !== 0 && (
|
subText="Connect Infisical to various 3rd party services and platforms."
|
||||||
<LearningItem
|
complete={false}
|
||||||
text="Integrate Infisical with your infrastructure"
|
icon={faPlug}
|
||||||
subText="Connect Infisical to various 3rd party services and platforms."
|
time="15 min"
|
||||||
complete={false}
|
link="https://infisical.com/docs/integrations/overview"
|
||||||
icon={faPlug}
|
/>
|
||||||
time="15 min"
|
)}
|
||||||
link="https://infisical.com/docs/integrations/overview"
|
</div>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
onOpenChange={(isModalOpen) => {
|
onOpenChange={(isModalOpen) => {
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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 }
|
||||||
|
];
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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";
|
||||||
|
Reference in New Issue
Block a user